Przekazywanie shared_ptr <Derived> jako shared_ptr <Base>

93

Jaka jest najlepsza metoda przekazywania a shared_ptrtypu pochodnego do funkcji, która przyjmuje shared_ptrtyp podstawowy?

Generalnie przekazuję shared_ptrje jako odniesienie, aby uniknąć niepotrzebnych kopii:

int foo(const shared_ptr<bar>& ptr);

ale to nie działa, jeśli spróbuję zrobić coś takiego

int foo(const shared_ptr<Base>& ptr);

...

shared_ptr<Derived> bar = make_shared<Derived>();
foo(bar);

mógłbym użyć

foo(dynamic_pointer_cast<Base, Derived>(bar));

ale wydaje się to nieoptymalne z dwóch powodów:

  • dynamic_castWydaje się nieco nadmierne dla prostego pochodzi do podstawy obsady.
  • Jak rozumiem, dynamic_pointer_casttworzy kopię (choć tymczasową) wskaźnika, aby przejść do funkcji.

Czy jest lepsze rozwiązanie?

Aktualizacja dla potomnych:

Okazało się, że był to problem z brakującym plikiem nagłówkowym. Poza tym to, co próbowałem tutaj zrobić, jest uważane za anty-wzór. Ogólnie,

  • Funkcje, które nie wpływają na czas życia obiektu (tj. Obiekt pozostaje ważny przez czas trwania funkcji) powinny przyjmować zwykłe odwołanie lub wskaźnik, np int foo(bar& b).

  • Funkcje, które konsumują obiekt (czyli są końcowymi użytkownikami danego obiektu) powinny przyjmować unique_ptrwartość, np int foo(unique_ptr<bar> b). Wzywający powinni std::movepodać wartość do funkcji.

  • Funkcje wydłużające żywotność obiektu powinny przyjmować shared_ptrwartość, np int foo(shared_ptr<bar> b). Obowiązuje zwyczajowa rada, aby unikać odwołań cyklicznych .

Szczegóły w artykule Herba Suttera Back to Basics .

Matt Kline
źródło
8
Dlaczego chcesz zdać shared_ptr? Dlaczego brak odniesienia do stałej baru?
ipc
2
Każda dynamicobsada jest potrzebna tylko do obniżenia. Również przekazanie wskaźnika pochodnego powinno działać dobrze. shared_ptrUtworzy nowy z tym samym refcount (i zwiększy go) i wskaźnikiem do bazy, która następnie zostanie powiązana z referencją const. Ponieważ jednak już bierzesz referencje, nie rozumiem, dlaczego shared_ptrw ogóle chcesz je wziąć . Weź Base const&i zadzwoń foo(*bar).
Xeo
@Xeo: Przekazywanie wskaźnika pochodnego (tj. foo(bar)) Nie działa, przynajmniej w MSVC 2010.
Matt Kline
1
Co masz na myśli mówiąc „oczywiście nie działa”? Kod kompiluje się i zachowuje poprawnie; pytasz, jak uniknąć tworzenia tymczasowego, shared_ptraby przejść do funkcji? Jestem prawie pewien, że nie da się tego uniknąć.
Mike Seymour
1
@Seth: Nie zgadzam się. Myślę, że jest powód, aby przekazywać wspólny wskaźnik według wartości, a nie ma powodu, aby przekazywać wspólny wskaźnik przez odniesienie (a wszystko to bez popierania niepotrzebnych kopii). Rozumowanie tutaj stackoverflow.com/questions/10826541/…
R. Martinho Fernandes

Odpowiedzi:

47

Chociaż Basei Derivedsą kowariantne, a surowe wskaźniki do nich będą działać odpowiednio shared_ptr<Base>i nieshared_ptr<Derived> są kowariantne. Jest to poprawny i najprostszy sposób rozwiązania tego problemu.dynamic_pointer_cast

( Edycja: static_pointer_cast byłoby bardziej odpowiednie, ponieważ rzutujesz z wersji pochodnej do bazy, co jest bezpieczne i nie wymaga sprawdzania w czasie wykonywania. Zobacz komentarze poniżej).

Jeśli jednak twoja foo()funkcja nie chce brać udziału w wydłużaniu czasu życia (a raczej brać udział we współwłasności obiektu), to najlepiej jest zaakceptować a const Base&i usunąć odniesienie do shared_ptrpodczas przekazywania go foo().

void foo(const Base& base);
[...]
shared_ptr<Derived> spDerived = getDerived();
foo(*spDerived);

Na marginesie, ponieważ shared_ptrtypy nie mogą być kowariantne, reguły niejawnych konwersji w kowariantnych typach zwracanych nie mają zastosowania podczas zwracania typów shared_ptr<T>.

Bret Kuhns
źródło
39
Nie są kowariantne, ale shared_ptr<Derived>można je niejawnie konwertować na shared_ptr<Base>, więc kod powinien działać bez rzucania shenaniganów.
Mike Seymour
9
Um, shared_ptr<Ty>ma konstruktor, który pobiera a shared_ptr<Other>i wykonuje odpowiednią konwersję, jeśli Ty*jest niejawnie konwertowany na Other*. A jeśli potrzebna jest obsada, static_pointer_castto jest odpowiednia, a nie dynamic_pointer_cast.
Pete Becker
To prawda, ale nie z jego parametrem odniesienia, jak w pytaniu. Mimo wszystko musiałby zrobić kopię. Ale jeśli używa shared_ptrodwołań do, aby uniknąć liczenia odwołań, to naprawdę nie ma dobrego powodu, aby używać shared_ptrw pierwszej kolejności. Najlepiej const Base&zamiast tego użyć .
Bret Kuhns
@PeteBecker Zobacz mój komentarz do Mike'a na temat konstruktora konwersji. Szczerze nie wiedziałem static_pointer_cast, dzięki.
Bret Kuhns
1
@TanveerBadar Nie jestem pewien. Być może nie udało się tego skompilować w 2012 roku? (w szczególności przy użyciu programu Visual Studio 2010 lub 2012). Ale masz absolutną rację, kod OP powinien absolutnie się kompilować, jeśli pełna definicja / publicznie wyprowadzonej / klasy jest widoczna dla kompilatora.
Bret Kuhns
32

Dzieje się tak również, jeśli zapomniałeś określić dziedziczenie publiczne w klasie pochodnej, tj. Jeśli tak jak ja napiszesz to:

class Derived : Base
{
};
dshepherd
źródło
classdotyczy parametrów szablonu; structsłuży do definiowania klas. (To co najwyżej 45% żartu.)
Davis Herring,
To zdecydowanie należy uznać za rozwiązanie, nie ma potrzeby obsady, ponieważ brakuje jej tylko publiczności.
Alexis Paques
12

Wygląda na to, że za bardzo się starasz. shared_ptrjest tani w kopiowaniu; to jeden z jego celów. Przekazywanie ich przez odniesienie tak naprawdę niewiele daje. Jeśli nie chcesz udostępniać, przekaż nieprzetworzony wskaźnik.

To powiedziawszy, są na to dwa sposoby, które przychodzą mi do głowy:

foo(shared_ptr<Base>(bar));
foo(static_pointer_cast<Base>(bar));
Pete Becker
źródło
9
Nie, nie są tanie w kopiowaniu, należy je przekazywać jako odniesienie, gdy tylko jest to możliwe.
Seth Carnegie
6
@SethCarnegie - czy Herb profilował swój kod, aby sprawdzić, czy przekazywanie wartości było wąskim gardłem?
Pete Becker
25
@SethCarnegie - to nie odpowiada na zadane przeze mnie pytanie. I co jest warte, napisałem shared_ptrimplementację, którą dostarcza Microsoft.
Pete Becker
6
@SethCarnegie - masz odwrotną heurystykę. Zasadniczo optymalizacji układów nie należy wykonywać, chyba że możesz wykazać, że są potrzebne.
Pete Becker
21
To tylko „przedwczesna” optymalizacja, jeśli trzeba nad tym popracować. Nie widzę problemu w przyjmowaniu skutecznych idiomów zamiast nieefektywnych, niezależnie od tego, czy robi to różnicę w określonym kontekście, czy nie.
Mark Ransom
11

Sprawdź również, czy #includeplik nagłówkowy zawierający pełną deklarację klasy pochodnej znajduje się w pliku źródłowym.

Miałem ten problem. Nie std::shared<derived>chciał rzucać std::shared<base>. Zadeklarowałem obie klasy, aby móc przechowywać do nich wskaźniki, ale ponieważ nie miałem #includekompilatora, nie mogłem zobaczyć, że jedna klasa pochodzi od drugiej.

Phil Rosenberg
źródło
1
Wow, nie spodziewałem się tego, ale to naprawiło to dla mnie. Byłem bardzo ostrożny i włączałem pliki nagłówkowe tylko tam, gdzie ich potrzebowałem, więc niektóre z nich były tylko w plikach źródłowych i dalej deklarując je w nagłówkach, jak powiedziałeś.
jigglypuff
Głupi kompilator jest głupi. To był mój problem. Dziękuję Ci!
Tanveer Badar