Jak zwrócić inteligentne wskaźniki (shared_ptr), przez odniesienie lub wartość?

96

Powiedzmy, że mam klasę z metodą, która zwraca shared_ptr.

Jakie są możliwe zalety i wady zwrotu na podstawie referencji lub wartości?

Dwie możliwe wskazówki:

  • Wczesne niszczenie obiektów. Jeśli shared_ptrzwrócę odwołanie by (const), licznik odwołań nie zostanie zwiększony, więc ponoszę ryzyko usunięcia obiektu, gdy wyjdzie on poza zakres w innym kontekście (np. W innym wątku). Czy to jest poprawne? A jeśli środowisko jest jednowątkowe, czy taka sytuacja może się również zdarzyć?
  • Koszt. Wartość dodana z pewnością nie jest darmowa. Czy w miarę możliwości warto tego unikać?

Dzięki wszystkim.

Vincenzo Pii
źródło

Odpowiedzi:

115

Zwraca inteligentne wskaźniki według wartości.

Jak powiedziałeś, jeśli zwrócisz go przez odniesienie, nie zwiększysz odpowiednio liczby referencji, co stwarza ryzyko usunięcia czegoś w niewłaściwym czasie. Już samo to powinno być wystarczającym powodem, aby nie wracać przez odniesienie. Interfejsy powinny być solidne.

Kwestia kosztów jest obecnie dyskusyjna dzięki optymalizacji wartości zwracanej (RVO), więc nie poniesiesz sekwencji zwiększania-zwiększania-zmniejszania lub czegoś podobnego w nowoczesnych kompilatorach. Dlatego najlepszym sposobem na zwrócenie a shared_ptrjest po prostu zwrócenie wartości:

shared_ptr<T> Foo()
{
    return shared_ptr<T>(/* acquire something */);
};

Jest to zupełnie oczywista okazja dla nowoczesnych kompilatorów C ++ w RVO. Wiem na pewno, że kompilatory Visual C ++ implementują RVO nawet wtedy, gdy wszystkie optymalizacje są wyłączone. W przypadku semantyki przenoszenia w C ++ 11 ten problem jest jeszcze mniej istotny. (Ale jedynym sposobem na upewnienie się jest profilowanie i eksperymentowanie).

Jeśli nadal nie jesteś przekonany, Dave Abrahams ma artykuł, który przedstawia argument za zwrotem według wartości. Odtwarzam tutaj fragment; Gorąco polecam przeczytanie całego artykułu:

Bądź szczery: jak się czujesz po poniższym kodzie?

std::vector<std::string> get_names();
...
std::vector<std::string> const names = get_names();

Szczerze mówiąc, chociaż powinienem wiedzieć lepiej, denerwuje mnie to. W zasadzie, kiedy get_names() wraca, musimy skopiować a vectorz strings. Następnie musimy skopiować go ponownie podczas inicjalizacji names i musimy zniszczyć pierwszą kopię. Jeśli stringw wektorze jest Ns, każda kopia może wymagać aż N + 1 alokacji pamięci i całej masy nieprzyjaznych dla pamięci podręcznej dostępów do danych, gdy zawartość ciągu jest kopiowana.

Zamiast konfrontować się z tego rodzaju niepokojem, często wracam do przekazywania odniesienia, aby uniknąć niepotrzebnych kopii:

get_names(std::vector<std::string>& out_param );
...
std::vector<std::string> names;
get_names( names );

Niestety, takie podejście jest dalekie od ideału.

  • Kod wzrósł o 150%
  • Musieliśmy upaść const upaść, ponieważ mutujemy imiona.
  • Jak lubią przypominać programiści funkcjonalni, mutacja sprawia, że ​​kod staje się bardziej skomplikowany, co do którego można wnioskować, podważając przejrzystość referencyjną i rozumowanie równania.
  • Nie mamy już ścisłej semantyki wartości dla nazw.

Ale czy naprawdę konieczne jest zepsucie naszego kodu w ten sposób, aby uzyskać wydajność? Na szczęście odpowiedź brzmi nie (a zwłaszcza nie, jeśli używasz C ++ 0x).

In silico
źródło
Nie wiem, czy powiedziałbym, że RVO sprawia, że ​​pytanie jest dyskusyjne, ponieważ powrót przez odniesienie zdecydowanie uniemożliwia RVO.
Edward Strange
@CrazyEddie: To prawda, to jeden z powodów, dla których zalecam, aby OP zwracały się według wartości.
In silico
Czy reguła RVO dozwolona przez normę ma pierwszeństwo przed regułami dotyczącymi synchronizacji / relacji zdarza się przed, gwarantowanych przez standard?
edA-qa mort-ora-y
1
@ edA-qa mort-ora-y: RVO jest wyraźnie dozwolone, nawet jeśli ma efekty uboczne. Na przykład, jeśli masz cout << "Hello World!";instrukcję w konstruktorze domyślnym i kopiującym, nie zobaczysz dwóch Hello World!sekund, gdy działa RVO. Nie powinno to jednak stanowić problemu dla odpowiednio zaprojektowanych inteligentnych wskaźników, nawet synchronizacji w trybie wrt.
In silico
23

Jeśli chodzi o dowolny inteligentny wskaźnik (nie tylko shared_ptr), nie sądzę, aby kiedykolwiek można było zwrócić odniesienie do jednego, i bardzo wahałbym się, czy przekazać je przez referencję lub surowy wskaźnik. Czemu? Ponieważ nie możesz być pewien, że nie zostanie on później skopiowany płytko przez odniesienie. Twój pierwszy punkt określa powód, dla którego powinno to budzić niepokój. Może się to zdarzyć nawet w środowisku jednowątkowym. Nie potrzebujesz jednoczesnego dostępu do danych, aby umieścić w swoich programach nieprawidłową semantykę kopiowania. Nie kontrolujesz tego, co Twoi użytkownicy robią ze wskaźnikiem, gdy go przekażesz, więc nie zachęcaj do niewłaściwego używania, dając użytkownikom API wystarczającą ilość liny, aby się powiesić.

Po drugie, spójrz na implementację swojego inteligentnego wskaźnika, jeśli to możliwe. Budowa i zniszczenie powinny być niemal pomijalne. Jeśli ten narzut jest nie do zaakceptowania, nie używaj inteligentnego wskaźnika! Ale poza tym będziesz musiał również zbadać architekturę współbieżności, którą masz, ponieważ wzajemnie wykluczający się dostęp do mechanizmu, który śledzi użycie wskaźnika, spowolni cię bardziej niż zwykłe zbudowanie obiektu shared_ptr.

Edytuj, 3 lata później: wraz z pojawieniem się bardziej nowoczesnych funkcji w C ++, poprawiłbym moją odpowiedź, aby była bardziej akceptowalna w przypadkach, gdy po prostu napisałeś lambdę, która nigdy nie żyje poza zakresem funkcji wywołującej i nie jest skopiowane gdzie indziej. Tutaj, gdybyś chciał zaoszczędzić minimalny koszt kopiowania współdzielonego wskaźnika, byłoby to sprawiedliwe i bezpieczne. Czemu? Ponieważ możesz zagwarantować, że odniesienie nigdy nie zostanie niewłaściwie użyte.

San Jacinto
źródło