Czy powinienem preferować wskaźniki lub odniesienia w danych członków?


138

Oto uproszczony przykład ilustrujący pytanie:

class A {};

class B
{
    B(A& a) : a(a) {}
    A& a;
};

class C
{
    C() : b(a) {} 
    A a;
    B b; 
};

Więc B jest odpowiedzialny za aktualizację części C. Przepuściłem kod przez lint i narzekał o elemencie referencyjnym: lint # 1725 . To mówi o dbaniu o domyślną kopię i przypisania, co jest wystarczająco uczciwe, ale domyślna kopia i przypisanie są również złe w przypadku wskaźników, więc nie ma z tego żadnej korzyści.

Zawsze staram się używać odnośników tam, gdzie mogę, ponieważ nagie wskaźniki wprowadzają niepewnie, kto jest odpowiedzialny za usunięcie tego wskaźnika. Wolę osadzać obiekty według wartości, ale jeśli potrzebuję wskaźnika, używam auto_ptr w danych składowych klasy, która jest właścicielem wskaźnika, i przekazuję obiekt jako odniesienie.

Generalnie użyłbym wskaźnika w danych składowych tylko wtedy, gdy wskaźnik mógłby być zerowy lub mógłby się zmienić. Czy są jakieś inne powody, aby preferować wskaźniki zamiast referencji dla członków danych?

Czy prawdą jest, że obiekt zawierający odniesienie nie powinien być przypisywany, ponieważ odwołanie nie powinno być zmieniane po zainicjowaniu?


„ale domyślna kopia i przypisanie są również złe w przypadku wskaźników”: To nie to samo: wskaźnik można zmienić w dowolnym momencie, jeśli nie jest stałą. Odwołanie jest zwykle zawsze stałe! (Zobaczysz to, jeśli spróbujesz zmienić swój element członkowski na „A & const a;”. Kompilator (przynajmniej GCC) ostrzeże Cię, że odwołanie jest i tak const, nawet bez słowa kluczowego „const”.
mmmmmmmm

Główny problem polega na tym, że jeśli ktoś wykona B b (A ()), masz spieprzone, ponieważ kończysz z wiszącym odniesieniem.
log0

Odpowiedzi:


67

Unikaj członków referencyjnych, ponieważ ograniczają to, co może zrobić implementacja klasy (w tym, jak wspomniałeś, zapobieganie implementacji operatora przypisania) i nie zapewniają żadnych korzyści z tego, co może zapewnić klasa.

Przykładowe problemy:

  • jesteś zmuszony zainicjować odniesienie na liście inicjalizatorów każdego konstruktora: nie ma możliwości rozłożenia tej inicjalizacji na inną funkcję ( do C ++ 0x, w każdym razie edytuj: C ++ ma teraz konstruktory delegujące )
  • odniesienie nie może zostać odbite ani być puste. Może to być zaletą, ale jeśli kod wymaga zmiany, aby umożliwić ponowne wiązanie lub aby element członkowski był pusty, wszystkie zastosowania elementu członkowskiego muszą zostać zmienione
  • w przeciwieństwie do członków wskaźnika, odwołań nie można łatwo zastąpić inteligentnymi wskaźnikami lub iteratorami, ponieważ może to wymagać refaktoryzacji
  • Zawsze, gdy używane jest odniesienie, wygląda jak typ wartości ( .operator itp.), Ale zachowuje się jak wskaźnik (może dyndać) - więc np. Przewodnik po stylach Google odradza go

66
Wszystkich rzeczy, o których wspomniałeś, warto unikać, więc jeśli pomagają w tym odniesienia - są dobre, a nie złe. Lista inicjalizacyjna to najlepsze miejsce do inicjalizacji danych. Bardzo często musisz ukrywać operator przypisania, z referencjami, których nie musisz. „Nie można się odbić” - dobrze, ponowne użycie zmiennych to zły pomysł.
Mykola Golubyev

4
@Mykola: Zgadzam się z tobą. Wolę, aby elementy członkowskie były inicjalizowane na listach inicjalizujących, unikam wskaźników zerowych iz pewnością nie jest dobre, aby zmienna zmieniała swoje znaczenie. Nasze opinie różnią się, ponieważ nie potrzebuję ani nie chcę, aby kompilator wymuszał to za mnie. Nie ułatwia to pisania klas, nie natknąłem się na błędy w tym obszarze, które by wyłapał, a czasami doceniam elastyczność, którą otrzymuję dzięki kodowi, który konsekwentnie używa elementów składowych wskaźnika (inteligentne wskaźniki w stosownych przypadkach).
James Hopkin

Myślę, że brak konieczności ukrywania przypisania jest fałszywym argumentem, ponieważ nie musisz ukrywać przypisania, jeśli klasa zawiera wskaźnik, że i tak nie jest odpowiedzialna za usunięcie - ustawienie domyślne tak. Myślę, że jest to najlepsza odpowiedź, ponieważ daje dobre powody, dla których wskaźniki powinny być preferowane przed odniesieniami w danych członków.
markh44

4
James, nie chodzi o to, że ułatwia pisanie kodu, ale o to, że jest on łatwiejszy do odczytania. Mając odwołanie jako element danych, nigdy nie musisz się zastanawiać, czy w momencie użycia może być zerowe. Oznacza to, że możesz spojrzeć na kod w mniejszym kontekście.
Len Holgate

4
To sprawia, że ​​testy jednostkowe i kpiny są znacznie trudniejsze, niemożliwe prawie w zależności od tego, ile z nich jest używanych, w połączeniu z „eksplozją wykresu wpływu” są wystarczającymi powodami IMHO, aby ich nie używać.
Chris Huang-Leaver

156

Moja własna praktyczna zasada:

  • Użyj elementu referencyjnego, jeśli chcesz, aby życie twojego obiektu zależało od życia innych obiektów : jest to wyraźny sposób, aby powiedzieć, że nie pozwalasz obiektowi żyć bez ważnej instancji innej klasy - z powodu braku przypisanie i obowiązek uzyskania inicjalizacji referencji za pośrednictwem konstruktora. To dobry sposób na zaprojektowanie klasy bez zakładania, że ​​jej instancja jest członkiem innej klasy lub nie. Zakładasz tylko, że ich życie jest bezpośrednio powiązane z innymi instancjami. Pozwala to później zmienić sposób korzystania z instancji klasy (z nową instancją lokalną, członkiem klasy, generowaną przez pulę pamięci w menedżerze itp.)
  • Użyj wskaźnika w innych przypadkach : jeśli chcesz później zmienić element członkowski, użyj wskaźnika lub wskaźnika do stałej, aby upewnić się, że odczytujesz tylko wskazaną instancję. Jeśli ten typ ma być kopiowalny, i tak nie możesz używać odwołań. Czasami trzeba również zainicjować element członkowski po wywołaniu funkcji specjalnej (na przykład init ()), a wtedy po prostu nie masz innego wyboru, jak tylko użyć wskaźnika. ALE: używaj potwierdzeń we wszystkich funkcjach składowych, aby szybko wykryć niewłaściwy stan wskaźnika!
  • W przypadkach, w których chcesz, aby czas życia obiektu był zależny od czasu życia obiektu zewnętrznego, a także potrzebujesz tego typu, aby można go było skopiować, użyj elementów członkowskich wskaźnika, ale argument odniesienia w konstruktorze W ten sposób wskazujesz na konstrukcji, że czas życia tego obiektu zależy na okres istnienia argumentu, ALE implementacja używa wskaźników, aby nadal można było je skopiować. Dopóki te elementy członkowskie są zmieniane tylko przez kopiowanie, a Twój typ nie ma domyślnego konstruktora, typ powinien spełniać oba cele.

1
Myślę, że twoje i Mykoły są poprawnymi odpowiedziami i promują programowanie bez wskaźnika (czytaj mniej wskaźników).
amit

37

Obiekty rzadko powinny pozwalać na przypisywanie i inne rzeczy, takie jak porównanie. Jeśli weźmiesz pod uwagę model biznesowy z takimi obiektami jak „Dział”, „Pracownik”, „Dyrektor”, trudno wyobrazić sobie przypadek, w którym jeden pracownik zostanie przydzielony drugiemu.

Dlatego w przypadku obiektów biznesowych bardzo dobrze jest opisywać relacje jeden do jednego i jeden do wielu jako odniesienia, a nie wskaźniki.

I prawdopodobnie można opisać relację jeden lub zero jako wskaźnik.

Więc nie ma „nie możemy przypisać”, a następnie czynnik.
Wielu programistów przyzwyczaja się po prostu do wskaźników i dlatego znajdą argument, aby uniknąć używania referencji.

Posiadanie wskaźnika jako członka zmusi Ciebie lub członka Twojego zespołu do wielokrotnego sprawdzania wskaźnika przed użyciem, z komentarzem „na wszelki wypadek”. Jeśli wskaźnik może mieć wartość zero, to prawdopodobnie wskaźnik jest używany jako rodzaj flagi, co jest złe, ponieważ każdy obiekt musi odgrywać swoją rolę.


2
+1: To samo, czego doświadczyłem. Tylko niektóre ogólne klasy przechowywania danych wymagają przypisania i skopiowania c'tor. W przypadku większej liczby struktur obiektów biznesowych wysokiego poziomu powinny istnieć inne techniki kopiowania, które również obsługują takie rzeczy, jak „nie kopiuj unikatowych pól” i „dodaj do innego elementu nadrzędnego” itp. Dlatego do kopiowania obiektów biznesowych należy używać struktury wysokiego poziomu i nie zezwalaj na przypisania niskiego poziomu.
mmmmmmmm

2
+1: Pewne punkty zgodności: członkowie referencyjni uniemożliwiający zdefiniowanie operatora przypisania nie jest ważnym argumentem. Jak poprawnie stwierdzisz w inny sposób, nie wszystkie obiekty mają semantykę wartości. Zgadzam się również, że nie chcemy, aby „if (p)” było rozproszone po całym kodzie bez logicznego powodu. Ale poprawnym podejściem do tego jest zastosowanie niezmienników klas: dobrze zdefiniowana klasa nie pozostawia miejsca na wątpliwości, czy składowe mogą być puste. Jeśli jakikolwiek wskaźnik może mieć wartość null w kodzie, spodziewałbym się, że zostanie to dobrze skomentowane.
James Hopkin

@JamesHopkin Zgadzam się, że dobrze zaprojektowane klasy mogą efektywnie używać wskaźników. Oprócz ochrony przed wartością NULL, odwołania gwarantują również, że odwołanie zostało zainicjowane (wskaźnik nie musi być inicjalizowany, więc może wskazywać na cokolwiek innego). Odnośniki mówią również o własności - a mianowicie, że obiekt nie jest właścicielem członka. Jeśli konsekwentnie używasz odwołań dla zagregowanych elementów członkowskich, będziesz mieć bardzo wysoki stopień pewności, że elementy składowe wskaźnika są obiektami złożonymi (chociaż nie ma wielu przypadków, w których element złożony jest reprezentowany przez wskaźnik).
weberc2


6

W kilku ważnych przypadkach przypisywalność po prostu nie jest potrzebna. Są to często lekkie opakowania algorytmów, które ułatwiają obliczenia bez opuszczania zakresu. Takie obiekty są głównymi kandydatami na członków referencyjnych, ponieważ możesz mieć pewność, że zawsze zawierają ważne referencje i nigdy nie trzeba ich kopiować.

W takich przypadkach upewnij się, że operator przypisania (a często także konstruktor kopiujący) nie nadaje się do użytku (przez dziedziczenie boost::noncopyablelub deklarowanie ich jako prywatne).

Jednakże, jak już skomentował użytkownik pts, to samo nie dotyczy większości innych obiektów. W tym przypadku korzystanie z członków referencyjnych może być ogromnym problemem i należy go zasadniczo unikać.


6

Ponieważ wszyscy wydają się rozdawać ogólne zasady, zaproponuję dwa:

  • Nigdy, przenigdy nie używaj odwołań jako członków klasy. Nigdy nie robiłem tego w swoim własnym kodzie (z wyjątkiem udowodnienia sobie, że miałem rację w tej regule) i nie wyobrażam sobie przypadku, w którym bym to zrobił. Semantyka jest zbyt zagmatwana i naprawdę nie do tego zostały zaprojektowane referencje.

  • Zawsze, zawsze używaj odwołań podczas przekazywania parametrów do funkcji, z wyjątkiem typów podstawowych lub gdy algorytm wymaga kopii.

Te zasady są proste i bardzo mi pomogły. Pozostawiam tworzenie zasad używania inteligentnych wskaźników (ale proszę, nie auto_ptr) jako członków klasy dla innych.


2
Nie do końca zgadzam się z pierwszym, ale brak porozumienia nie jest kwestią wartości uzasadnienia. +1
David Rodríguez - dribeas

(Wydaje się jednak, że przegrywamy ten argument z dobrymi wyborcami SO)
James Hopkin

2
Oddałem głos, ponieważ „Nigdy, przenigdy nie używaj odwołań jako członków klasy”, a poniższe uzasadnienie nie jest dla mnie właściwe. Jak wyjaśniono w mojej odpowiedzi (i komentarzu do najlepszej odpowiedzi) iz własnego doświadczenia, są przypadki, w których jest to po prostu dobra rzecz. Myślałem tak, jak ty, dopóki nie zostałem zatrudniony w firmie, w której używali go w dobry sposób i wyjaśniło mi, że są przypadki, w których to pomaga. W rzeczywistości uważam, że prawdziwy problem polega bardziej na składni i ukrytych implikacjach, które sprawiają, że użycie odwołań jako członków wymaga od programisty zrozumienia tego, co robi.
Klaim

1
Neil> Zgadzam się, ale to nie „opinia” zmusiła mnie do odrzucenia głosu, to stwierdzenie „nigdy”. Ponieważ argumentacja tutaj nie wydaje się pomocna, anuluję mój głos negatywny.
Klaim

2
Tę odpowiedź można poprawić, wyjaśniając, co w przypadku semantyki elementów referencyjnych jest mylące. To uzasadnienie jest ważne, ponieważ na pierwszy rzut oka wskaźniki wydają się bardziej niejednoznaczne semantycznie (mogą nie być zainicjowane i nie mówią nic o własności obiektu bazowego).
weberc2

4

Tak na: Czy prawdą jest, że obiekt zawierający odniesienie nie powinien być przypisywany, ponieważ odwołanie nie powinno być zmieniane po zainicjowaniu?

Moje praktyczne zasady dla członków danych:

  • nigdy nie używaj odwołania, ponieważ zapobiega to przypisaniu
  • jeśli twoja klasa jest odpowiedzialna za usuwanie, użyj metody boost scoped_ptr (która jest bezpieczniejsza niż auto_ptr)
  • w przeciwnym razie użyj wskaźnika lub wskaźnika stałego

2
Nie potrafię nawet nazwać przypadku, w którym potrzebowałbym przypisania obiektu biznesowego.
Mykola Golubyev

1
+1: Dodam tylko, aby uważać na członków auto_ptr - każda klasa, która je posiada, musi mieć wyraźnie zdefiniowane przypisanie i konstrukcję kopiowania (wygenerowane są bardzo mało prawdopodobne, aby były tym, co jest wymagane)
James Hopkin

auto_ptr zapobiega również (rozsądnemu) przypisaniu.

2
@Mykola: Doświadczyłem również, że 98% moich obiektów biznesowych nie wymaga cesji ani kopiowania. Zapobiegam temu przez małe makro, które dodaję do nagłówków tych klas, co sprawia, że ​​kopiowanie c'tors i operator = () są prywatne bez implementacji. Więc w 98% obiektów używam również referencji i jest to bezpieczne.
mmmmmmmm

1
Odwołania jako elementy członkowskie mogą służyć do jawnego stwierdzenia, że ​​żywotność wystąpienia klasy jest bezpośrednio zależna od czasu życia wystąpień innych klas (lub tego samego). „Nigdy nie używaj odwołania, ponieważ zapobiega przypisaniu” zakłada, że ​​wszystkie klasy muszą zezwalać na przypisanie. To tak, jakby założyć, że wszystkie klasy muszą zezwalać na kopiowanie ...
Klaim

2

Generalnie użyłbym wskaźnika w danych składowych tylko wtedy, gdy wskaźnik mógłby być zerowy lub mógłby się zmienić. Czy są jakieś inne powody, aby preferować wskaźniki zamiast referencji dla członków danych?

Tak. Czytelność Twojego kodu. Wskaźnik sprawia, że ​​jest bardziej oczywiste, że element członkowski jest referencją (jak na ironię :)), a nie obiektem zawartym, ponieważ kiedy go używasz, musisz go usunąć. Wiem, że niektórzy myślą, że jest to staroświeckie, ale nadal uważam, że to po prostu zapobiega pomyłkom i błędom.


1
Lakos również argumentuje za tym w „Projektowaniu oprogramowania w C ++ na dużą skalę”.
Johan Kotlinski

Uważam, że Code Complete również to popiera i nie jestem temu przeciwny. Nie jest to jednak zbyt atrakcyjne ...
markh44

2

Odradzam członkom danych referencyjnych, ponieważ nigdy nie wiadomo, kto będzie pochodził z Twojej klasy i co mogą chcieć zrobić. Mogą nie chcieć korzystać z obiektu, do którego się odwołuje, ale będąc odniesieniem, zmusiłeś ich do podania prawidłowego obiektu. Zrobiłem to sobie na tyle, aby przestać używać członków danych referencyjnych.


0

Jeśli dobrze rozumiem pytanie ...

Odwołanie jako parametr funkcji zamiast wskaźnika: Jak zauważyłeś, wskaźnik nie wyjaśnia, kto jest właścicielem czyszczenia / inicjalizacji wskaźnika. Preferuj punkt współdzielony, gdy chcesz mieć wskaźnik, jest to część C ++ 11 i słaby_ptr, gdy ważność danych nie jest gwarantowana przez cały okres istnienia klasy akceptującej wskazane dane .; również część C ++ 11. Użycie odwołania jako parametru funkcji gwarantuje, że referencja nie jest pusta. Aby obejść ten problem, musisz obalić funkcje języka, a nas nie obchodzą luźne kodery dział.

Odniesienie do zmiennej składowej: Jak wyżej w odniesieniu do ważności danych. Gwarantuje to, że wskazane dane, a następnie przywoływane, są ważne.

Przeniesienie odpowiedzialności za ważność zmiennej do wcześniejszego punktu w kodzie nie tylko czyści późniejszy kod (w naszym przykładzie klasa A), ale także czyni go jasnym dla osoby używającej.

W twoim przykładzie, który jest nieco zagmatwany (naprawdę spróbuję znaleźć jaśniejszą implementację), A używane przez B jest gwarantowane przez cały okres życia B, ponieważ B jest członkiem A, więc odniesienie to wzmacnia i jest (być może) bardziej jasne.

I na wszelki wypadek (co jest mało prawdopodobne, ponieważ nie miałoby to żadnego sensu w kontekście twojego kodu), inna alternatywa, użycie parametru A bez odniesienia, bez wskaźnika, skopiowałaby A i uczyniłaby paradygmat bezużytecznym - ja naprawdę nie myśl, że masz to na myśli jako alternatywa.

Ponadto gwarantuje to również, że nie będziesz w stanie zmienić danych, do których się odwołujesz, gdzie można zmodyfikować wskaźnik. Wskaźnik do stałej działałby tylko wtedy, gdyby odnośnik / wskaźnik do danych nie był zmienny.

Wskaźniki są przydatne, jeśli nie można zagwarantować ustawienia parametru A dla B lub można go ponownie przypisać. Czasami słaby wskaźnik jest po prostu zbyt rozwlekły dla implementacji, a duża liczba osób albo nie wie, co to jest słaby_ptr, albo po prostu ich nie lubi.

Rozerwij tę odpowiedź, proszę :)

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.