Приведение общего указателя к производному классу с дополнительной функциональностью - это безопасно?

Рассмотрим следующий план:

class Base { /* ... */ };

class Derived : public Base
{
public:
    void AdditionalFunctionality(int i){ /* ... */ }
};

typedef std::shared_ptr<Base> pBase;
typedef std::shared_ptr<Derived> pDerived;

int main(void)
{
    std::vector<pBase> v;
    v.push_back(pBase(new Derived()));

    pDerived p1(  std::dynamic_pointer_cast<Derived>(v[0])  ); /* Copy */
    pDerived p2 = std::dynamic_pointer_cast<Derived>(v[0]);    /* Assignment */

    p1->AdditionalFunctionality(1);
    p2->AdditionalFunctionality(2);

    /* A */

    return 0;
}

Здесь я расширяю базовый класс производным классом, который добавляет функциональность (метод AdditionalFunctionality).

Первый вопрос, это нормально? Я прочитал много вопросов, в которых говорится, что это не нормально, и вам следует объявить дополнительную функциональность в базовом классе (часто предлагается сделать их чистыми виртуальными методами в базовом классе). Однако я не хочу этого делать. Я хочу расширить функциональность базового класса, а не просто реализовать его по-другому. Есть ли лучшее решение для достижения этой цели?

Хорошо, поэтому в этом коде я также использую контейнер STL для хранения этих указателей, который позволяет мне хранить указатели как на объекты типа Base, так и на объекты типа Derived, не разрезая объекты.

Второй вопрос, это имеет смысл, правда? Фактически, я избегаю нарезки, используя указатели на объекты базового класса, а не сами объекты базового класса?

Если я «знаю», что определенный указатель относится к производному объекту, я использую std::dynamic_pointer_cast для приведения интеллектуального указателя.

Третий вопрос: это компилируется без предупреждения и работает, но безопасно ли это? Действительный? Будет ли он нарушать подсчет ссылок общих указателей и не сможет delete мои объекты или delete их раньше, чем я ожидаю?

Наконец, я могу выполнить это приведение с помощью конструктора копирования или присваивания, как показано для p1 и p2. Есть ли предпочтительный / правильный способ сделать это?

Похожие вопросы:

  • Понижение уровня shared_ptr ‹Base› до shared_ptr ‹Derived›?: это очень близко , однако класс dervied не добавляет дополнительных функций, как у меня, поэтому я не уверен, что это полностью то же самое. Кроме того, он использует boost::shared_ptr, где я использую std::shared_ptr (хотя я понимаю, что boost пожертвовал shared_ptr библиотеке std, поэтому они, вероятно, такие же).

Спасибо за помощь.


Изменить:

Одна из причин, по которой я спрашиваю, заключается в том, что я понимаю, что можно сделать следующее (неправильно):

    /* Intentional Error */
    v.push_back(pBase(new Base()));
    pDerived p3( std::dynamic_pointer_cast<Derived>(v[1]) );
    p3->AdditionalFunctionality(3); /* Note 1 */

Когда я пытаюсь преобразовать указатель на объект Base в указатель на объект Derived, а затем вызвать метод, который реализован только в классе Derived. Другими словами, указанный объект не определяет (или даже не «знает» о методе).

Это не обнаруживается компилятором, но может вызвать segfault в зависимости от того, как определено AdditionalFunctionality.


person jedwards    schedule 08.06.2011    source источник
comment
pDerived p2 не является присваиванием, он по-прежнему копирует (или, по крайней мере, вызывает конструктор копирования, если это то, что вы имеете в виду под copy и assignment)   -  person Kiril Kirov    schedule 09.06.2011
comment
@Kiril Kirov спасибо, что отвечает на этот вопрос.   -  person jedwards    schedule 09.06.2011
comment
@Mahesh - Я не уверен, о чем вы спрашиваете, но могу вас заверить, что v.push_back(pBase(new Derived())); компилируется. Фактически, пока я понижаю значение указателей, которые изначально были преобразованы в повышении, программа работает правильно.   -  person jedwards    schedule 09.06.2011
comment
@jedwards - о вашем редактировании - если вы нажмете только один элемент и затем приведете v[1], это вызовет ошибку seg, да, поскольку v[1] end итератор. Но это могла быть просто опечатка.   -  person Kiril Kirov    schedule 09.06.2011
comment
Единственное, чего не хватает вашему коду, - это проверок, чтобы убедиться, что экземпляры pDerived непусты, перед их разыменованием.   -  person ildjarn    schedule 09.06.2011
comment
@Kiril Kirov - Извините за непонятность - этот раздел должен уместиться там, где я только что добавил комментарий /* A */ в исходный код. Значит, индекс 1 находится в пределах допустимого диапазона.   -  person jedwards    schedule 09.06.2011


Ответы (3)


Есть ли у Base виртуальный деструктор? Если да, то можно использовать понижающее преобразование. В вашем неверном образце результат pDerived должен быть NULL, поэтому вам нужно каждый раз проверять результат dynamic_pointer_cast.

person Kirill V. Lyadvinsky    schedule 08.06.2011
comment
У Base есть виртуальный деструктор, и в неверном примере pDerived фактически NULL. if(pDerived)... предотвращает ошибку сегментации. Спасибо. - person jedwards; 09.06.2011
comment
Технически ему не нужен виртуальный деструктор, потому что конструктор shared_ptr является шаблоном и запоминает исходный тип, использованный для его создания. Я бы не советовал злоупотреблять этим. - person Mark B; 09.06.2011
comment
@Mark B, вы правы. Но виртуальный деструктор позволит вам использовать понижающее преобразование при использовании других интеллектуальных указателей. - person Kirill V. Lyadvinsky; 09.06.2011

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

Если контейнер может иметь оба типа объектов, то кажется, что вы хотите иметь возможность обрабатывать все объекты как базовый класс в этом контейнере. В этом случае вы почти наверняка захотите использовать полиморфизм, чтобы поступать правильно: иметь виртуальный интерфейс, который в основном говорит «Сделай эту работу», а родительская версия может вообще ничего не делать. Затем дочерняя версия метода реализует необходимые вам дополнительные функции.

Я думаю, вы можете почувствовать запах кода, что ваши объекты менее связаны, чем вы думаете. Вы наследуете, чтобы использовать повторно или разрешить замену? Вы также можете пересмотреть, как выглядит ваш публичный интерфейс.

Все сказанное, если вы решите продолжить свой текущий дизайн (который я бы, по крайней мере, тщательно рассмотрел), я думаю, что ваше понижающее преобразование должно быть безопасным, если вы проверяете результат динамического приведения на ненулевое значение перед его использованием.

person Mark B    schedule 08.06.2011

Хорошо, во-первых, если вы это сделаете, вы захотите убедиться, что у Base есть виртуальный деструктор. В противном случае вы получите неопределенное поведение, когда вектор выйдет за пределы области видимости. (Деструктор вектора вызовет деструктор Base для каждого из своих элементов. Если какие-либо элементы действительно являются Derived - KABOOM!) Кроме того, то, что вы написали, совершенно безопасно и достоверно.

Но какой в ​​этом смысл? Если у вас есть контейнер с объектами, вы хотите иметь возможность обращаться с ними одинаково. (Переберите их все и вызовите функцию для каждого или что-то еще.) Если вы не хотите относиться к ним одинаково, зачем помещать их в один контейнер? Итак, у вас есть вектор, который может содержать указатели на Base или указатели на Derived - как узнать, какие элементы какого типа? Планируете ли вы просто вызывать dynamic_cast для каждого элемента каждый раз, когда хотите вызвать AdditionalFunctionality, чтобы убедиться, что элемент действительно указывает на Derived? Это ни эффективно, ни идиоматично, и в основном сводит на нет весь смысл использования наследования. С таким же успехом вы могли бы просто использовать помеченное объединение.

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

person Josh    schedule 08.06.2011
comment
+1 за второй и третий абзацы, которые раскрывают суть проблемы. - person Mark B; 09.06.2011
comment
Идея состоит в том, чтобы расширить Base на несколько различных производных классов. Для этого мне нужно, чтобы в контейнере содержались указатели базового класса. - person jedwards; 09.06.2011