Większość odpowiedzi tutaj nie rozwiązuje wewnętrznej dwuznaczności posiadania surowego wskaźnika w sygnaturze funkcji, jeśli chodzi o wyrażanie zamiaru. Problemy są następujące:
Program wywołujący nie wie, czy wskaźnik wskazuje pojedynczy obiekt, czy początek „tablicy” obiektów.
Dzwoniący nie wie, czy wskaźnik „posiada” pamięć, na którą wskazuje. IE, czy funkcja powinna zwolnić pamięć. ( foo(new int)
- Czy to wyciek pamięci?).
Dzwoniący nie wie, czy nullptr
można bezpiecznie przejść do funkcji.
Wszystkie te problemy są rozwiązywane przez referencje:
Odnośniki zawsze odnoszą się do jednego obiektu.
Referencje nigdy nie posiadają pamięci, do której się odnoszą, są jedynie widokiem na pamięć.
Referencje nie mogą mieć wartości zerowej.
To sprawia, że referencje są znacznie lepszym kandydatem do ogólnego użytku. Jednak referencje nie są idealne - należy rozważyć kilka głównych problemów.
- Brak wyraźnej pośrednictwa. Nie jest to problem z surowym wskaźnikiem, ponieważ musimy użyć
&
operatora, aby pokazać, że rzeczywiście mijamy wskaźnik. Na przykład int a = 5; foo(a);
tutaj wcale nie jest jasne, że a jest przekazywane przez odniesienie i może zostać zmodyfikowane.
- Nullability. Ta słabość wskaźników może być również siłą, gdy faktycznie chcemy, aby nasze referencje były zerowalne. Uznanie za
std::optional<T&>
nieważne (z dobrych powodów), wskaźniki dają nam taką pożądalność.
Wydaje się więc, że kiedy chcemy nullownego odniesienia z wyraźną pośrednią interwencją, powinniśmy sięgnąć po T*
prawo? Źle!
Abstrakcje
W naszej desperacji na punkcie zerowania możemy sięgnąć T*
i po prostu zignorować wszystkie niedociągnięcia i semantyczne dwuznaczności wymienione wcześniej. Zamiast tego powinniśmy sięgnąć po to, co C ++ robi najlepiej: abstrakcję. Jeśli po prostu napiszemy klasę, która owija się wokół wskaźnika, zyskujemy wyrazistość, a także nullability i wyraźną pośredniość.
template <typename T>
struct optional_ref {
optional_ref() : ptr(nullptr) {}
optional_ref(T* t) : ptr(t) {}
optional_ref(std::nullptr_t) : ptr(nullptr) {}
T& get() const {
return *ptr;
}
explicit operator bool() const {
return bool(ptr);
}
private:
T* ptr;
};
Jest to najprostszy interfejs, jaki mogłem wymyślić, ale działa on skutecznie. Pozwala na zainicjowanie odwołania, sprawdzenie, czy wartość istnieje i dostęp do wartości. Możemy go używać w następujący sposób:
void foo(optional_ref<int> x) {
if (x) {
auto y = x.get();
// use y here
}
}
int x = 5;
foo(&x); // explicit indirection here
foo(nullptr); // nullability
Osiągnęliśmy nasze cele! Zobaczmy teraz zalety w porównaniu do surowego wskaźnika.
- Interfejs pokazuje wyraźnie, że odwołanie powinno odnosić się tylko do jednego obiektu.
- Najwyraźniej nie jest właścicielem pamięci, do której się odnosi, ponieważ nie ma niszczyciela zdefiniowanego przez użytkownika ani metody usuwania pamięci.
- Program wywołujący wie, że
nullptr
można go przekazać, ponieważ autor funkcji wyraźnie prosi o polecenieoptional_ref
Możemy sprawić, że interfejs stanie się bardziej złożony, na przykład dodając operatory równości, interfejs monadyczny get_or
i map
interfejs, metodę, która pobiera wartość lub zgłasza wyjątek, constexpr
obsługę. Możesz to zrobić przez ciebie.
Podsumowując, zamiast używać surowych wskaźników, uzasadnij, co te wskaźniki faktycznie oznaczają w kodzie, albo wykorzystaj standardową abstrakcję biblioteki lub napisz własną. Znacząco poprawi to Twój kod.
new
utworzeniu wskaźnika i wynikających z tego kwestiach własności.