Страницы

Поиск по вопросам

воскресенье, 15 марта 2020 г.

Возможно ли “виртуальное клонирование” в С++?

#cpp #gcc


Наткнулся в "More Exceptional C++" на информацию, что виртуальные методы могут вместо
указателей на объекты базового класса возвращать объекты перегруженного класса. Решил
попробовать смоделировать "самоопределяющийся" объект - с виртуальным методом, вместо
указателя базового класса возвращающим указатель на объект производного, чтобы можно
было вызывать функции, отсутствующие в базовом классе. Вот пример с "виртуальным клонированием":
#include 
#include 

class Base
{
public:
    Base(){}
    virtual ~Base()=0;
    virtual Base*   clone()
    {
        qDebug()<<"Base clone";
        return 0;
    }
};

Base::~Base(){}

class Derived: public Base
{
public:
    Derived(){}
    virtual Derived*   clone()
    {
        qDebug()<<"Derived clone";
        return new Derived(*this);
    }
    QString derivedOnly(){return "derivedOnly";}
    virtual ~Derived(){}
};

int main(int argc, char *argv[])
{
    Derived d1;
    Base *  b1=&d1;
    Base *  b2=b1->clone();     b2=b2;
    Derived * d2=d1.clone();    delete d2;
//  Derived * d3=b1->clone();   delete d3;
}

Так всё работает, и вывод qDebug показывает, что запускается клонирование производного
класса. Однако, раскомментировав последнюю строку, при компиляции получаю сообщение
об ошибке "invalid conversion from 'Base' to 'Derived' [-fpermissive]".
Выводы:

Очевидно, что компилятор (g++, Windows/MinGW) проигнорировал описание Derived * Derived::clone(),
сгенерировав вместо него код для Base * Derived::clone().
Эту подмену транслятор сделал, никак меня не уведомив. Я рассчитываю получить указатель
типа Derived, а получаю Base - и ничего об этом не знаю! 
Я понимаю, для чего это сделано. Я мог бы написать что-нибудь типа b1->clone()->derivedOnly(),
и транслятор не знал бы, допустима ли такая конструкция, так как статически неизвестно,
будет ли при исполнении b1 указывать на Base или на Derived.

Вопросы:

Почему так происходит - является ли это частью стандарта (где можно посмотреть?)
или самоуправством g++ (4.6.2)?
Можно ли сделать так, чтобы выдавался warning?
Ведут ли себя так же другие трансляторы?
Есть ли возможность решить изначальную задачу, то есть "развернуть" объект из указателя
на базовый класс, не указывая, в какой именно класс мы его разворачиваем (т. е. без
dynamic_cast'ов)?
    


Ответы

Ответ 1



О выводах Выводы неверные. Пункт 3 верный. Он же является ответом на Ваш вопрос. А вот пункт 1 и, следовательно, 2 - неверные. Derived* Derived::clone(); Base* Base::clone(); Какой из методов Вы бы выбрали, будь у Вас указатель на тип Base*? Ответы 1) Вы же сами отвечаете себе на этот вопрос в выводе 3: статически неизвестно, будет ли при исполнении b1 указывать на Base или на Derived Это приводит к тому, что компилятор использует статический тип объекта, то есть Base*. У объекта такого типа метод Base* clone() возвращает Base*, чтобы привести его к Derived* нужно явное преобразование. Ссылка на стандарт: раздел 10.3.8: [...] its result is converted to the type returned by the (statically chosen) overridden function (5.2.2) 2) Тут я поразмышляю о другом warning'е. Который не будет показан, если написать, например: Derived* d2 = dynamic_cast( b1->clone() ); Явное преобразование static_cast( b1->clone() ) это как бы уже warning. Страуструп пишет в "Дизайн и эволюция C++", что намеренно завел вместо лаконичного, но скрытного синтаксиса приведения типов C, громоздкий и неудобный, привлекающий внимание синтаксис приведения типов в стиле C++ (с _cast<>'ами). Кто Вам гарантирует, что b1 всегда будет указывать на Derived-объект? Только Вы сами и гарантируете. За этот архитектурный "косяк" (см. п.4) Вы и расплачиваетесь dynamic_cast'ом. Или, если производительность важна, static_cast'ом, подкладывая себе мину замедленного действия. Относитесь к явным преобразованиям типов, как к warning'ам, ругающим Вашу архитектуру. 3) да. См. п. 1 Например, VS2013: error C2440: 'initializing' : cannot convert from 'Base *' to 'Derived *' Cast from base to derived requires dynamic_cast or static_cast 4) Итак, Вы хотите реализовать виртуальный метод клонирования. Сам метод реализован правильно, и работает правильно. Неправильно Вы используете результат клонирования - считаете, что там какой-то определенный потомок. На самом деле, имея указатель Base*, Вы можете полагаться только на интерфейс класса Base, никаких QString derivedOnly(). Как же быть? Вариант 0. Вы точно знаете, что Base* в данном контексте - это Derived*. Тогда Вы можете либо сразу указать тип Derived*, либо сделать dynamic_cast. Вариант1. Код оперирует указателем Base* и неизвестно, на какой из наследников есть указатель: Derived1* или Derived2*. Вы клонируете объект, думая, что там Derived1, а на самом деле не он. Что тогда? Логика программы развалилась. Из-за того, что Вы полагаетесь на различный интерфейс сущностей, которые по контракту все имеют единый интерфейс Base*.

Комментариев нет:

Отправить комментарий