C++, Вызов виртуального метода после завершения конструктора суперкласса

350
24 июля 2017, 15:22

Слегка отрефакторил код своей библиотечки сетевого обмена. Обсуждаемая часть - классы запросов и ответов. Для пересылки по сети эти классы сериализуются/десериализуются.

Общая структура запросов такая: абстрактный класс, несколько его наследников, класс-builder с методами fromByteArray и toByteArray, у ответов - аналогично. Абстрактный класс и его наследники имеют ID, по которому builder понимает, какой именно запрос/ответ десериализовывать.

В изначальной реализации builder считывал ID из потока данных и проходил таким кодом:

if (Request1().typeID()==ID)
{
    /* создаём Request1 */ 
}
else if ()...

и так далее. Недостаток в том, что при добавлении новых запросов нужно модифицировать builder. Поэтому я завёл в buildere статическую карту, отображающую ID в handler типа std::function, и статическую функцию registerHandler. Теперь все типы-потомки в конструкторе имеют запись вида:

{
    RequestBuilder::registerHandler(typeID(),[](QDataStream & is)
    {
        // код, десериализующий экземпляр этого класса.
    });
}

Теперь builder просто проверяет, есть ли пришедший ID в его карте, и запускает соответствующий handler:

if (!handlers().contains(ID))
    // ругаемся
else
    return handlers()[ID](is);

Теперь собственно проблема, совет по решению которой нужен. Программист, расширяющий мою библиотеку, может забыть добавить код для десериализации. Код для сериализации я сделал чисто абстрактным в базовом классе, и не переопределить его нельзя, программа не скомпилируется. А вот код для десериализации, даже если его аналогично реализовать, легко забыть добавить в таблицу handlerов. Сделать привязку handlerа в конструкторе базового класса тоже нельзя, потому что вызовется он, когда производный класс ещё не создан, и привяжет ID и handler базового класса. Есть ли какой-то способ гарантировать регистрацию handlerа в производных классах?

Update: сгенерировал тестовый код, проверил идею @Sergey Slepov:

#include <iostream>
#include <functional>
#include <memory>
#include <map>
using namespace std;
class Base
{
public:
    using PBase=std::shared_ptr<Base>;
    using Handler=std::function<PBase()>;
    Base(int ID,Handler handler)
    {cout<<"Base constructing ID "<<ID<<endl;handlers()[ID]=handler;}
    ~Base(){}
    virtual int     typeId(){return 0;}
    virtual void    whoami(){cout<<"I am base."<<endl;}
    static PBase    instance(int ID){return handlers()[ID]();}
private:
    static map<int,Handler> &handlers() {static map<int,Handler> m; return m;}
};
class Derived:public Base
{
public:
    Derived():Base(typeId(),[](){return PBase(new Derived());})
    {cout<<"Derived constructing"<<endl;}
    ~Derived(){}
    virtual int     typeId(){return 1;}
    virtual void    whoami(){cout<<"I am Derived."<<endl;}
};
int main(int /*argc*/, char **/**argv[]*/)
{
    //Derived d;
    Base::instance(1)->whoami();
    return 0;
 }

В этом примере карту handlerов хранит базовый класс, для простоты примера, в остальном всё почти как в рабочем коде. Всё работает, как задумано, если раскомментировать первую строчку в main.

Таким образом, проблему удалось уменьшить, но не победить. Теперь программист, расширяющий библиотеку, не может не указать функтор для десериализации, так как иначе код не соберётся. Но он всё ещё может забыть создать хотя бы один экземпляр класса до использования карты.

Предложенные ниже методы с использованием CRTP мне нравятся тем, что позволяют вынести задачу регистрации handlerа в отдельный интерфейс, а не тащить её в базовый класс. Но всё равно создавать хотя бы один экземпляр класса придётся.

Вообще становится ясно, что с использованием модифицируемых классов типа map задача не решится. Для того, чтобы в него добавить handler, надо выполнить код, а это во время компиляции не сделать. Видимо, если такой способ и есть, искать его надо где-то в шаблонной магии товарища Alexandrescu. Его я читал, но полностью понять не осилил.

Answer 1

Если я все правильно понял, то я бы сделал так.
Для начала вынесем все что нам надо в интерфейс:

class IObject{
public:
    virtual void doWork() = 0;
    virtual void serialize() const = 0;
    virtual void deserialize()  = 0;
    virtual ~IObject(){}
};

Теперь сделаем шаблонного строителя:

class IObjectBuilder{
public:
    virtual IObject* build() const = 0;
    virtual ~IObjectBuilder(){}
};
template<class Object>
class ObjectBuilder : public IObjectBuilder{
public:
    Object* build() const{
        return new Object();
    }
};

Далее реализуем фабрику-синглтон, которая будет создавать объекты в зависимости от некого идентификатора:

class ObjectFactory{
    typedef std::unique_ptr<IObjectBuilder> BuilderPointer;
    typedef std::map<int, BuilderPointer> BilderMap;
    BilderMap _builders;
    ObjectFactory(){}
public:
    //Синглтон
    static ObjectFactory& instance(){
        static ObjectFactory instance;
        return instance;
    }
    //Регистрация типа в фабрике. 
    //Для успешной регистрации нужно:
    // 1) чтобы Object наследовал IObject
    // 2) У Object был енум или интовая константа с именем Rtti
    template<class Object>
    int reg(){
        int type = Object::Rtti;
        _builders[type].reset(new ObjectBuilder<Object>());
        return type;
    }
    //Создание объекта по типу
    IObject* build(int id) const{
        IObjectBuilder *builder = _findBuilder(id);
        if(!builder){
            return 0;
        }
        return builder->build();
    };
private:
    IObjectBuilder* _findBuilder(int id) const{
        BilderMap::const_iterator builder = _builders.find(id);
        if(builder == _builders.end()){
            return 0;
        }
        return builder->second.get();
    }
};

Вообщем-то на этом можно было бы остановиться. Просто регистрируем новые типы в фабрике, по мере того как они создаются, и горя не знаем. Но вы хотите чтобы все происходило автоматически. Для этого я бы использовал CRTP

Создаем абстрактный класс, который при создании первого объекта будет регистрировать тип наследника в фабрике:

template<class Derived>
class AbstractObject : public IObject{
public:
    AbstractObject(){
        static const int type = ObjectFactory::instance().reg<Derived>();
    }
};

Ну а теперь пример, как пользоваться всем этим зоопарком. Создаем пару наследников:

class ObjectA : public AbstractObject<ObjectA>{
public:
    enum{
        Rtti = 1
    };
    void doWork(){
        std::cout << "ObjectA::doWork\n";
    }
    void serialize() const{
        std::cout << "ObjectA::serialize\n";
    }
    void deserialize(){
        std::cout << "ObjectA::deserialize\n";
    }
};
class ObjectB : public AbstractObject<ObjectB>{
public:
    enum{
        Rtti = 2 
    };
    void doWork(){
        std::cout << "ObjectB::doWork\n";
    }
    void serialize() const{
        std::cout << "ObjectB::serialize\n";
    }
    void deserialize(){
        std::cout << "ObjectB::deserialize\n";
    }
};

И используем:

int main(){
    ObjectA a; //Важно. Тип автоматически регистриуется в фабрике при создании первого экземпляра
    ObjectB b;
    std::unique_ptr<IObject> object;
    object.reset(ObjectFactory::instance().build(ObjectA::Rtti));
    object->serialize();
    object->deserialize();
    object->doWork();
    object.reset(ObjectFactory::instance().build(ObjectB::Rtti));
    object->serialize();
    object->deserialize();
    object->doWork();
}

Полный пример

Answer 2

Можно сделать так: потребовать от потомков передачи в конструктор базового класса информации о своем типе и зарегистрировать этот тип в RequestBuilder:

#include <string>
#include <functional>
#include <memory>
using namespace std;
class RequestBase
{
public:
    virtual ~RequestBase() = 0;
    virtual void Serialize(class QDataStream &) = 0;
};
typedef unique_ptr<RequestBase> DeserializeRequest (QDataStream &);
struct RequestBuilder
{
    static void registerHandler(const string & name, DeserializeRequest deserialize)
    {
    }
};
template<class T>
class Request : public RequestBase
{
protected:
    Request()
    {
        RequestBuilder::registerHandler(typeid(T).name(), T::Deserialize);
    }
};
class Request1 : public Request<Request1>
{
public:
    void Serialize(class QDataStream &) override
    {
    }
    static unique_ptr<RequestBase> Deserialize(QDataStream &)
    {
        return make_unique<Request1>();
    }
};

Теперь каждый потомок обязан иметь Serialize и Deserialize.

READ ALSO
Спецификатор формата для типа bool в fwscanf

Спецификатор формата для типа bool в fwscanf

Как считать данные типа bool из файла при помощи fwscanf? Какие спецификаторы использовать?

271
WinAPI, рассчитать ширину шрифта для TextOut

WinAPI, рассчитать ширину шрифта для TextOut

Как правильно рассчитать точную ширину выводимого символа текущего шрифта, чтобы вывести текст с помощью TextOut в рабочее окно? Делаю для полосы...

264
Время выполнения функций

Время выполнения функций

Вот, я создал constexpr функциюКак точно определить, выполнится она во время компиляции или в runtime?

208