Передача shared_ptr <Derived> как shared_ptr <Base>

93

Каков наилучший способ передать a shared_ptrпроизводного типа функции, которая принимает shared_ptra базового типа?

Я обычно передаю shared_ptrs по ссылке, чтобы избежать ненужной копии:

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

но это не сработает, если я попытаюсь сделать что-то вроде

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

...

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

Я мог бы использовать

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

но это кажется неоптимальным по двум причинам:

  • A dynamic_castкажется немного чрезмерным для простого приведения типа производного к основному.
  • Насколько я понимаю, dynamic_pointer_castсоздает копию (пусть и временную) указателя для перехода к функции.

Есть ли лучшее решение?

Обновление для потомков:

Оказалось, проблема в отсутствующем файле заголовка. Кроме того, то, что я пытался здесь сделать, считается антипаттерном. В общем-то,

  • Функции, которые не влияют на время жизни объекта (т. Е. Объект остается действующим на время выполнения функции), должны принимать простую ссылку или указатель, например int foo(bar& b).

  • Функции, которые потребляют объект (т.е. являются конечными пользователями данного объекта), должны принимать unique_ptrпо значению, например int foo(unique_ptr<bar> b). Вызывающие должны указать std::moveзначение в функции.

  • Функции, продлевающие время жизни объекта, должны принимать shared_ptrпо значению, например int foo(shared_ptr<bar> b). Применяется обычный совет избегать циклических ссылок .

См. Доклад Херба Саттера « Назад к основам» .

Мэтт Клайн
источник
8
Почему вы хотите сдать shared_ptr? Почему нет постоянной ссылки бара?
ipc
2
Любое dynamicприведение нужно только для понижения. Кроме того, передача производного указателя должна работать нормально. Он создаст новый shared_ptrс тем же refcount (и увеличит его) и указатель на базу, который затем привяжется к ссылке const. Однако, поскольку вы уже берете ссылку, я не понимаю, почему вы вообще хотите ее использовать shared_ptr. Возьми Base const&и позвони foo(*bar).
Xeo
@Xeo: Передача производного указателя (т.е. foo(bar)) не работает, по крайней мере, в MSVC 2010.
Мэтт Клайн
1
Что вы имеете в виду под «очевидно, не работает»? Код компилируется и ведет себя правильно; вы спрашиваете, как избежать создания временного объекта shared_ptrдля передачи функции? Я почти уверен, что этого не избежать.
Майк Сеймур,
1
@Seth: Я не согласен. Я думаю, что есть причина передавать общий указатель по значению, и очень мало причин передавать общий указатель по ссылке (и все это без защиты ненужных копий). Рассуждения здесь stackoverflow.com/questions/10826541/…
Р. Мартиньо Фернандес

Ответы:

47

Хотя Baseи Derivedявляются ковариантными, но необработанные указатели на них будут действовать соответственно shared_ptr<Base>и неshared_ptr<Derived> являются ковариантными. Это самый правильный и простой способ справиться с этой проблемой.dynamic_pointer_cast

( Изменить: static_pointer_cast было бы более подходящим, потому что вы выполняете преобразование из производного в базовый, что безопасно и не требует проверок во время выполнения. См. Комментарии ниже.)

Однако, если ваша foo()функция не желает принимать участие в продлении времени жизни (или, скорее, участвовать в совместном владении объектом), то лучше всего принять const Base&и разыменовать shared_ptrпри передаче в foo().

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

Кстати, поскольку shared_ptrтипы не могут быть ковариантными, правила неявных преобразований между ковариантными возвращаемыми типами не применяются при возврате типов shared_ptr<T>.

Брет Кунс
источник
39
Они не ковариантны, но shared_ptr<Derived>могут быть неявно преобразованы в shared_ptr<Base>, поэтому код должен работать без махинаций приведения типов.
Майк Сеймур
9
У гм, shared_ptr<Ty>есть конструктор, который принимает shared_ptr<Other>и выполняет соответствующее преобразование, если Ty*он неявно конвертируется в Other*. А если нужна гипсовая повязка, static_pointer_castто подходящей здесь нет dynamic_pointer_cast.
Pete Becker
Верно, но не с его эталонным параметром, как в вопросе. В любом случае ему нужно будет сделать копию. Но если он использует refs to shared_ptr, чтобы избежать подсчета ссылок, то на самом деле нет веских причин использовать a shared_ptrв первую очередь. Лучше использовать const Base&вместо этого.
Bret Kuhns
@PeteBecker См. Мой комментарий Майку о конструкторе преобразования. Честно говоря, не знал static_pointer_cast, спасибо.
Bret Kuhns
1
@TanveerBadar Не уверен. Возможно, это не удалось скомпилировать в 2012 году? (в частности, используя Visual Studio 2010 или 2012). Но вы абсолютно правы, код OP должен обязательно компилироваться, если полное определение / публично производного / класса видно компилятору.
Брет Кунс,
32

Это также произойдет, если вы забыли указать публичное наследование производного класса, т.е. если вы, как и я, напишите это:

class Derived : Base
{
};
dshepherd
источник
classдля параметров шаблона; structдля определения классов. (Это не более 45% шутки.)
Дэвис Херринг,
Это определенно следует рассматривать как решение, нет необходимости в приведении, так как он только пропускает public.
Алексис Пакес,
12

Похоже, вы слишком стараетесь. shared_ptrдешево копировать; это одна из его целей. Передача их по ссылке на самом деле не дает многого. Если вы не хотите делиться, передайте необработанный указатель.

Тем не менее, есть два способа сделать это, о которых я могу подумать:

foo(shared_ptr<Base>(bar));
foo(static_pointer_cast<Base>(bar));
Пит Беккер
источник
9
Нет, копировать их недешево, их следует по возможности передавать по ссылке.
Сет Карнеги
6
@SethCarnegie - профилировал ли Херб ваш код, чтобы увидеть, является ли передача по значению узким местом?
Pete Becker
25
@SethCarnegie - это не ответ на мой вопрос. И, как ни крути, я написал shared_ptrреализацию, поставляемую Microsoft.
Pete Becker
6
@SethCarnegie - у вас есть обратная эвристика. Ручные оптимизации, как правило, не следует проводить, если вы не можете показать, что они необходимы.
Pete Becker
21
Это только «преждевременная» оптимизация, если над ней нужно работать. Я не вижу проблем в выборе эффективных идиом вместо неэффективных, независимо от того, имеет ли это значение в конкретном контексте или нет.
Марк Рэнсом
11

Также убедитесь, что #includeфайл заголовка, содержащий полное объявление производного класса, находится в исходном файле.

У меня была такая проблема. То std::shared<derived>бы не снимали std::shared<base>. Я заранее объявил оба класса, чтобы я мог хранить указатели на них, но поскольку у меня не было #includeкомпилятора, компилятор не мог видеть, что один класс является производным от другого.

Фил Розенберг
источник
1
Вау, я этого не ожидал, но это исправило для меня. Я был очень осторожен и включал файлы заголовков только там, где они мне нужны, поэтому некоторые из них были только в исходных файлах и пересылаем их объявление в заголовках, как вы сказали.
jigglypuff
Тупой компилятор тупой. Это была моя проблема. Спасибо!
Танвир Бадар,