Odwołuj się do zmiennych składowych jako członków klasy

85

W moim miejscu pracy ten styl jest szeroko stosowany: -

#include <iostream>

using namespace std;

class A
{
public:
   A(int& thing) : m_thing(thing) {}
   void printit() { cout << m_thing << endl; }

protected:
   const int& m_thing; //usually would be more complex object
};


int main(int argc, char* argv[])
{
   int myint = 5;
   A myA(myint);
   myA.printit();
   return 0;
}

Czy jest jakaś nazwa opisująca ten idiom? Zakładam, że ma to zapobiec możliwie dużym obciążeniom związanym z kopiowaniem dużego złożonego obiektu?

Czy jest to ogólnie dobra praktyka? Czy są jakieś pułapki w tym podejściu?

Angus Comber
źródło
4
Jedną z możliwych pułapek jest sytuacja, gdy obiekt, do którego masz odniesienie w zmiennej składowej, zostanie zniszczony w innym miejscu i spróbujesz uzyskać do niego dostęp za pośrednictwem swojej klasy
mathematician1975

Odpowiedzi:

116

Czy jest jakaś nazwa opisująca ten idiom?

W UML nazywa się to agregacją. Różni się od kompozycji tym, że obiekt członkowski nie jest własnością klasy odsyłającej. W C ++ możesz zaimplementować agregację na dwa różne sposoby, poprzez odwołania lub wskaźniki.

Zakładam, że ma to zapobiec możliwie dużym obciążeniom związanym z kopiowaniem dużego złożonego obiektu?

Nie, to byłby naprawdę zły powód, aby tego używać. Głównym powodem agregacji jest to, że obiekt zawarty nie jest własnością obiektu zawierającego, a zatem ich okresy życia nie są ograniczone. W szczególności okres istnienia obiektu, do którego odwołuje się odwołanie, musi przeżyć ten, który się odwołuje. Mógł zostać utworzony znacznie wcześniej i może żyć dłużej niż do końca życia kontenera. Poza tym stan obiektu, do którego się odwołujemy, nie jest kontrolowany przez klasę, ale może zmieniać się zewnętrznie. Jeśli odniesienie nie jest const, klasa może zmienić stan obiektu, który żyje poza nią.

Czy jest to ogólnie dobra praktyka? Czy są jakieś pułapki w tym podejściu?

To narzędzie do projektowania. W niektórych przypadkach będzie to dobry pomysł, w innych nie. Najczęstszą pułapką jest to, że czas życia obiektu zawierającego odniesienie nigdy nie może przekraczać czasu życia obiektu, do którego się odwołuje. Jeśli otaczający obiekt użyje odwołania po zniszczeniu obiektu, do którego się odwołuje, będziesz mieć niezdefiniowane zachowanie. Ogólnie lepiej jest preferować kompozycję od agregacji, ale jeśli jej potrzebujesz, jest to równie dobre narzędzie, jak każde inne.

David Rodríguez - dribeas
źródło
7
„Nie, to byłby naprawdę zły powód, aby tego używać”. Czy mógłbyś rozwinąć tę kwestię? Co można zamiast tego wykorzystać, aby to osiągnąć?
coincoin
@coincoin: Co dokładnie osiągnąć?
David Rodríguez - dribeas
3
to prevent the possibly large overhead of copying a big complex object?
coincoin
3
@underscore_d dzięki za odpowiedź. A co się dzieje, gdy nie możesz użyć żadnego z nich. Wyobraź sobie, że chcemy udostępniać ten sam obiekt w różnych klasach. Skończysz z kopią obiektu dla każdej klasy, jeśli przekażesz ten obiekt członkowski według wartości? Dlatego rozwiązaniem jest użycie inteligentnych wskaźników lub odniesień, aby uniknąć kopiowania. Nie?
coincoin
2
@ plats1 właśnie napisałem. Wiem o tym. Chodzi mi o to, że możesz użyć inteligentnych wskaźników lub referencji.
coincoin
37

Nazywa się to iniekcją zależności przez iniekcję konstruktora : klasa Apobiera zależność jako argument do swojego konstruktora i zapisuje odwołanie do klasy zależnej jako zmienną prywatną.

Na Wikipedii jest ciekawe wprowadzenie .

Dla stałej poprawności napisałbym:

using T = int;

class A
{
public:
  A(const T &thing) : m_thing(thing) {}
  // ...

private:
   const T &m_thing;
};

ale problem z tą klasą polega na tym, że akceptuje ona odwołania do obiektów tymczasowych:

T t;
A a1{t};    // this is ok, but...

A a2{T()};  // ... this is BAD.

Lepiej dodać (wymaga przynajmniej C ++ 11):

class A
{
public:
  A(const T &thing) : m_thing(thing) {}
  A(const T &&) = delete;  // prevents rvalue binding
  // ...

private:
  const T &m_thing;
};

W każdym razie, jeśli zmienisz konstruktora:

class A
{
public:
  A(const T *thing) : m_thing(*thing) { assert(thing); }
  // ...

private:
   const T &m_thing;
};

jest prawie pewne, że nie będziesz mieć wskaźnika do tymczasowego .

Ponadto, ponieważ konstruktor pobiera wskaźnik, dla użytkowników jest jasne A, że muszą zwracać uwagę na czas życia przekazywanego obiektu.


Powiązane tematy to:

manlio
źródło
20

Czy jest jakaś nazwa opisująca ten idiom?

Nie ma nazwy dla tego zastosowania, jest po prostu znane jako „Odniesienie jako element członkowski klasy” .

Zakładam, że ma to zapobiec możliwie dużym obciążeniom związanym z kopiowaniem dużego złożonego obiektu?

Tak, a także scenariusze, w których chcesz powiązać czas życia jednego obiektu z innym obiektem.

Czy jest to ogólnie dobra praktyka? Czy są jakieś pułapki w tym podejściu?

Zależy od sposobu użytkowania. Korzystanie z dowolnej funkcji językowej przypomina „wybieranie koni na kursy” . Należy zauważyć, że istnieją wszystkie ( prawie wszystkie ) funkcje językowe, ponieważ są przydatne w niektórych scenariuszach.
Podczas używania odwołań jako członków klasy należy zwrócić uwagę na kilka ważnych kwestii:

  • Musisz upewnić się, że dany obiekt będzie istniał, dopóki nie będzie istniał obiekt klasy.
  • Musisz zainicjować element członkowski na liście inicjatorów elementu członkowskiego konstruktora. Nie możesz mieć leniwej inicjalizacji , co mogłoby być możliwe w przypadku elementu wskaźnika.
  • Kompilator nie wygeneruje przypisania kopii operator=()i będziesz musiał podać go samodzielnie. Określenie, jakie działanie =ma podjąć w takim przypadku operator, jest uciążliwe . W zasadzie twoja klasa staje się nieprzenoszalna .
  • Odwołania nie mogą być NULLani nie mogą odnosić się do żadnego innego obiektu. Jeśli potrzebujesz ponownego osadzenia, nie jest to możliwe z odniesieniem, jak w przypadku wskaźnika.

Dla większości praktycznych celów (chyba że naprawdę martwisz się wysokim zużyciem pamięci ze względu na rozmiar elementu członkowskiego) wystarczy mieć instancję elementu członkowskiego, zamiast wskaźnika lub elementu referencyjnego. Oszczędza to wiele zmartwień o inne problemy, które przynoszą członkowie referencji / wskaźnika kosztem dodatkowego zużycia pamięci.

Jeśli musisz użyć wskaźnika, upewnij się, że używasz inteligentnego wskaźnika zamiast surowego wskaźnika. To znacznie ułatwiłoby życie dzięki wskazówkom.

Alok Save
źródło
Zakładam, że ma to na celu zapobieżenie ewentualnemu dużemu narzutowi związanemu z kopiowaniem dużego złożonego obiektu? Tak” - proszę wyjaśnić, dlaczego uważasz, że ten wzorzec ma jakikolwiek związek z unikaniem kopiowania, ponieważ żadnego nie widzę. jeśli ten nie-`` idiom '' w jakiś sposób zapisuje kopie dla kogokolwiek jako efekt uboczny jego rzeczywistych celów, to ich oryginalny projekt był fatalnie uszkodzony, a samo zastąpienie go na miejscu tym wzorem raczej nie przyniesie tego, czego oczekują .
underscore_d
@underscore_d Powiedzmy, że klasa potrzebuje niebanalnej ilości danych stałych i może istnieć wiele wystąpień tej klasy w tym samym czasie. Posiadanie własnej kopii tych danych może być nie do przyjęcia dla każdej instancji. Tak więc zapisanie odniesienia do stałej w zewnętrznej lokalizacji tych danych, które można udostępniać, oszczędza wiele kopii. shared_ptr niekoniecznie jest rozwiązaniem, ponieważ uchwyt danych niekoniecznie musi być przydzielany dynamicznie.
Nieważne
@Unimportant Nie jestem pewien, jaki był mój sprzeciw w 2016 roku. Cały czas używam referencji jako członkowie klasy. Może obawiałem się, że zwykłe zastąpienie własności wartościami odniesieniami doprowadziłoby do pytań na całe życie i niekoniecznie jest panacaea lub czymś, co zawsze można zrobić 1: 1. Nie wiem.
underscore_d
1

C ++ zapewnia dobry mechanizm zarządzania czasem życia obiektu poprzez konstrukcje klasy / struktury. Jest to jedna z najlepszych cech C ++ w porównaniu z innymi językami.

Gdy zmienne składowe są uwidocznione przez ref lub wskaźnik, zasadniczo narusza to hermetyzację. Idiom ten umożliwia konsumentowi klasy zmianę stanu obiektu A bez posiadania przez niego (A) jakiejkolwiek wiedzy lub kontroli. Umożliwia także konsumentowi utrzymanie odniesienia / wskaźnika do stanu wewnętrznego A poza okresem życia obiektu A. To zły projekt. Zamiast tego klasa mogłaby zostać refaktoryzowana, aby przechowywać ref / wskaźnik do współdzielonego obiektu (nie być jego właścicielem) i można je ustawić za pomocą konstruktora (Mandate the life time rules). Klasa obiektu współdzielonego może być zaprojektowana do obsługi wielowątkowości / współbieżności, w zależności od przypadku.

Indy9000
źródło
2
ale kod w PO nie zawiera odniesienia do żadnej zmiennej składowej. ma zmienną składową, która jest referencją. więc świetne punkty, ale pozornie niezwiązane.
underscore_d
-3

Odniesienia do członków są zwykle uważane za złe. Utrudniają życie w porównaniu ze wskazówkami dla członków. Ale nie jest to szczególnie niezwykłe, ani nie jest to jakiś specjalny nazwany idiom lub rzecz. To tylko aliasowanie.

Szczeniak
źródło
23
Czy możesz podać odniesienia do wsparcia, które są zwykle uważane za złe ?
David Rodríguez - dribeas