Прехвърляне на споделен указател към производен клас с допълнителна функционалност - това безопасно ли е?

Помислете за следната схема:

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›? : Това е много близо , но извлеченият клас не добавя допълнителна функционалност като моята, така че не съм сигурен, че е напълно същият. Освен това използва 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 */

Когато се опитвам да намаля указател към основен обект към указател на производен обект и след това извиквам метод, който е внедрен само в производния клас. С други думи, посоченият обект не дефинира (или дори не е "наясно" с метода).

Това не се улавя от компилатора, но може да причини 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 fault, да, тъй като v[1] е end итератор. Но може да е просто печатна грешка.   -  person Kiril Kirov    schedule 09.06.2011
comment
Единственото нещо, което липсва на вашия код, са проверките, за да се гарантира, че екземплярите pDerived не са празни, преди да ги дереферирате.   -  person ildjarn    schedule 09.06.2011
comment
@Кирил Киров -- Съжалявам, че не съм ясен -- този раздел трябваше да се побере там, където току-що добавих коментара /* 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, прав си. Но виртуалният деструктор ще ви позволи да използвате downcasting, когато използвате други интелигентни указатели. - person Kirill V. Lyadvinsky; 09.06.2011

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

Ако контейнерът може да има и двата типа обекти, тогава изглежда, че искате да можете да третирате всички обекти като основен клас в този контейнер. В този случай почти със сигурност искате да използвате полиморфизма, за да направите правилното нещо: Имате виртуален интерфейс, който основно казва „Направете тази работа“ и родителската версия може да не прави нищо. Тогава дъщерната версия на метода имплементира допълнителната функционалност, от която се нуждаете.

Мисля, че може да усетите кода, че вашите обекти са по-малко свързани, отколкото си мислите. Наследявате ли, за да използвате повторно или за да позволите заместване? Може също така да искате да преразгледате как изглежда вашият публичен интерфейс.

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

person Mark B    schedule 08.06.2011

Добре, първо, ако направите това, ще искате да сте сигурни, че Base има виртуален деструктор. В противен случай ще получите недефинирано поведение, когато векторът излезе извън обхвата. (Деструкторът на вектора ще извика деструктора на Base за всеки от неговите елементи. Ако някои елементи наистина са Derived -- КАБУМ!) Освен това, това, което сте написали, е напълно безопасно и валидно.

Но какъв е смисълът? Ако имате контейнер с обекти, искате да можете да ги третирате еднакво. (Преминете през всички тях и извикайте функция за всеки, или каквото и да е друго.) Ако не искате да ги третирате еднакво, защо да ги поставяте в един контейнер? Така че имате вектор, който може да съдържа указатели към 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