All streams
Search
Write a publication
Pull to refresh
6
0
Send message
Дополню предыдущий комментарий. Предположим, что вам ни как нельзя иметь один абстрактный базовый класс. (Как вы и указали) Хорошо. Можно немного поэкспериментировать и сделать так:
Код
struct SomeObject1;
struct SomeObject2;
struct SomeObject3;

struct Interface1;
struct Interface2;

using SomeObjectList = TypeList< SomeObject1, SomeObject2, SomeObject3 >;

struct Interface1 : Dispatchable< SomeObjectList > 
{
    virtual void action1() = 0;
};
struct Interface2 : Dispatchable< SomeObjectList > 
{
    virtual void action2() = 0;
};

struct SomeObject1: Interface1, Interface2 
{
    DISPATCHED( SomeObject1, SomeObjectList )

    void action1() override {}
    void action2() override {}
};
struct SomeObject2: Interface1 
{
    DISPATCHED( SomeObject2, SomeObjectList )

    void action1() override {}
};
struct SomeObject3: Interface2
{
    DISPATCHED( SomeObject3, SomeObjectList )

    void action2() override {}
};

template< class T >
void testCommon( T& obj )
{
    /*Common*/
}

int main()
{
    SomeObject1 o1;
    SomeObject2 o2;
    SomeObject3 o3;

    std::vector< Interface1* > vector1 = { &o1, &o2 };
    std::vector< Interface2* > vector2 = { &o1, &o3 };

    for( auto* obj : vector1 )
    {
        obj->action1();
        obj->dispatch([](auto& obj){ testCommon( obj );});
    }

    for( auto* obj : vector2 )
    {
        obj->action2();
        obj->dispatch([](auto& obj){ testCommon( obj );});
    }
}


Подчеркну, что для такого случая данный подход не предназначался изначально.
В статье приведено описание того когда применять подход.
Его можно применять когда есть базовый класс/абстрактный базовый класс.
Используя ваше описание классов с интерфейсами, например можно сделать так:
Код
struct SomeObject1;
struct SomeObject2;
struct SomeObject3;

using SomeObjectList = TypeList< SomeObject1, SomeObject2, SomeObject3 >;

struct Interface1 
{
    virtual void action1() = 0;
};
struct Interface2 
{
    virtual void action2() = 0;
};

struct AbstractSomeObject : Dispatchable< SomeObjectList > 
{
    virtual ~AbstractSomeObject() = default;
};

struct SomeObject1: AbstractSomeObject, Interface1, Interface2 
{
    DISPATCHED( SomeObject1, SomeObjectList )

    void action1() override {}
    void action2() override {}
};
struct SomeObject2: AbstractSomeObject, Interface1 
{
    DISPATCHED( SomeObject2, SomeObjectList )

    void action1() override {}
};
struct SomeObject3: AbstractSomeObject, Interface2
{
    DISPATCHED( SomeObject3, SomeObjectList )

    void action2() override {}
};

template< class T >
void testCommon( T& obj )
{
    /*Common*/
}

int main()
{
    SomeObject1 o1;
    SomeObject2 o2;
    SomeObject3 o3;

    std::vector< AbstractSomeObject* > vector = { &o1, &o2, &o3 };

    for( auto* obj : vector )
    {
        obj->dispatch( []( auto& obj ) { testCommon(obj); } );
    }
}


Ситуация с оптимизациями такая, что соптимизировав в одном месте, мы можем потерять где-то в другом.

Согласен.

На счет нужных и не нужных данных, это уже зависит исключительно от конкретной задачи.
Разумеется бывает, что в класс попадает то что ему совсем не надо из за наследования, здесь уже нужно в каждом конкретном случае думать о том, а нужно ли вообще здесь наследование, зачем эти классы так связаны, как хотим укладывать данные в памяти и что вообще хотим получить.
Ну давайте во-первых разберемся, зачем вообще нужно имя name в классе.

У классов SomeObject1 и SomeObject2 нету общих полей.

1. Поле name было приведено в качестве примера общего поля для потомков класса BaseObject.
В данном случае это поле name предназначено для хранения имени конкретного объекта.
Потомки это SomeObject1 и SomeObject2, для них будет общее поле name и общая функция getName().

В-третьих, даже если вам прямо необходимо вынести общую функциональность в базовый класс,

2. Вы вынесли общее поле (name), я то же это сделал в примере который привел в предыдущем комментарии.
Но вы не вынесли здесь общую функциональность (функция getName).
Смотрите, если у вас будет N потомков от BaseObject, вам надо будет писать N раз функцию getName().
Достаточно ее один раз написать в базовом классе.

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

3. Мы сейчас рассматриваем конкретную иерархию из трех классов: BaseObject, SomeObject1, SomeObject2.
А так же возможное расширение этой иерархии, не более.

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

4. Вы дублирете поле name и функцию getName() в классах SomeObject1 и SomeObject2. В коде я оставил комментарий, где собственно происходит дублирование.

Ну вот вы в этой фразе и нарушили принцип SRP

5. Вы могли бы развернуто ответить, каким образом здесь нарушается SRP?

Про производительность, вы сами в примере «Ваш случай: №2» показали, как ее можно улучшить

6. Вы согласны с утверждением что вызов (в моем случае) обычной функции проще и дешевле, чем вызов функции обертки (в вашем случае), в которой вызывается
std::visit и уже в которой вызывается собственно сама функция?
Производительность диспатчеризации сопоставима, так зачем мне пользоваться оберткой?
Примеры в статье я привел.

Поэтому нету смысла говорить об общих, не общих полях. В каждом классе поля свои, и только свои.

7. Вы в своем же примере показали, когда приводили код, что это не так. BaseObject как раз и содержит общие поля.

Что мешает разделить такой класс на 2 части — с интерфейсом и общими полями?

8. Мне не ясно зачем это делать (в данном конкретном случае). Что вам не нравится в классе BaseObject с общим полем name и общей функцией getName?
Положим появились еще класс наследник только уже от SomeObject1.
В языке С++ уже есть механизм наследования, чтобы наследник мог получить доступ к полям SomeObject1 и специфичным для SomeObject1 функциям.

В том смысле, что объекты класса SomeObject1 и SomeObject2 не могут обратиться к полям друг друга

9. К специфичным полям, которые определены в самих класса SomeObject1 и SomeObject2 — нет, к полям, которые определены в классе BaseObject — да. Ведь они для них общие.

Вы не могли бы привести полностью развернутый пример кода т.е. включая иерархию из BaseObject, SomeObject1, SomeObject2 и вашего класса обертки и с разделением класса BaseObject на 2 части?
Дополнил статью сравнением производительности и типичным примером использования, спасибо svr_91 за хороший вопрос.

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

Положим у нас имеется простая иерархия классов, где каждый класс имеет поле «имя»
и какое то дополнительное поведение (функция action) в зависимости от своего типа.

Мой случай
struct BaseObject
{
    /* contructors */

    //общая функция для всех потомков
    std::string_view getName() const { return name; } 

    //строго говоря, эта функция не обязана быть чисто виртуально, мы вполне может сделать
    //базовое определение
    virtual void action() = 0; 
    //virtual void action() { /* BaseObject specific code */} 

protected:
    std::string name; //общее поле для всех потомков
};

struct SomeObject1 : BaseObject
{
    /* contructors */
    
    void action() override { /* SomeObject1 specific code */ }
    
protected:
    //имеем доступ к name 
};

struct SomeObject2 : BaseObject
{
    /* contructors */
    
    void action() override { /* SomeObject2 specific code */ }
    
protected:
    //имеем доступ к name
};

template< class T >
void test( T& obj )
{
    /*some code*/
}

std::vector< BaseObject* > vector = { /* some objects */ };

//Что хотел бы по пунктам:
for( auto* obj : vector )
{
    //1. вызывать обычную функцию базового класса напрямую (без оберток)
    std::cout « obj->getName();
    //2. вызывать виртуальную функцию (без оберток)
    obj->action();
    //3. а вот тут нам понадобился реальный тип, чтобы вызвать шаблон функции.
    //И об этом как раз статья и написана
    //obj->dispatch( [](auto&) { test( obj ); } );
}



Ваш случай: №1
struct SomeObject1
{
    /* contructors */
    std::string_view getName() const { return name; } //ok
    void action() { /* specific SomeObject1 code */ } //ok
    
protected:
    std::string name; //ok
};

struct SomeObject2
{
    /* contructors */
	//дублируем код (В SomeObject1 уже есть такая функция )
    std::string_view getName() const { return name; }
    void action() { /* specific SomeObject2 code */ } //ok
    
protected:
    std::string name; //дублируем код (В SomeObject1 уже есть такая функция)
};

struct Variant //Ваш класс обертка
{
    /* contructors */
    void getName() const { std::visit([]( auto&& obj) { return obj.getName(); }, var ) }
    void action() { std::visit([]( auto&& obj) { return obj.getName(); }, var ); }
    
    template< class Functor >
    void dispatch( Functor functor )
    {
    /* ... */
    }
    
private:
    std::variant< Object1, Object2 > var;
};

std::vector< Variant > vector = { /* some objects */ };

//Проводим парарелль
for( auto& var : vector )
{
    //1. вызываем функцию обертки, в которой вызываем std::visit и только потом вызываем getName.
    std::cout « var.getName();
    //2. тоже что и в 1
    var.action();
    //3. вытаскиваем тип тип из обертки и передали в шаблон функции
    var.dispatch( [](auto&) { test( obj ); } );
}



Ваш случай: №2
struct SomeObject1 //ok код не дублируется
{
    /* contructors */
    void action() { /* specific SomeObject1 code */ }
};

struct SomeObject2 //ok код не дублируется
{
    /* contructors */
    void action() { /* specific SomeObject2 code */ }
};

struct SamePart //Допустим
{
    std::string name;
};

struct Variant //Ваш класс обертка
{
    /* contructors */
    std::string_view getName() const { return samePart.name; }
    void action() { std::visit([]( auto&& obj) { return obj.action(); }, var ); }
    
    template< class Functor >
    void dispatch( Functor functor )
    {
    /* ... */
    }
    
private:
    SamePart samePart;
    std::variant< Object1, Object2 > var;
};



Проблемы:
1. Если иерархия у вас будет чуть более сложной, то что вы будете делать с общими полями всех классов?
(Под более сложной я понимаю, что в дереве наследования могут быть поддеревья, корни которых тоже могут иметь общие поля только уже для поддерева.)
Как и зачем вы будете решать эту проблему?
Для этого в языке C++ уже есть необходимый механизм — наследование.
(Да бывают ситуации, когда наследование можно заменить композицией)

2. SomeObject1 и SomeObject2 ничего не знают об имени,
но ведь по смыслу как раз они должны этим именем владеть.
И к остальным общим полям SomeObject1 и SomeObject2 тоже должны иметь доступ,
их поведение ведь может от этих полей зависеть, не зря ведь мы их собственно сделали общими.

3. Дублирование кода

4. Производительность. Обращение к функциям-членам базового класса происходит через несколько слоев косвенности, тоже самое про виртуальные функции. В моем же случае, вызов происходит напрямую.
Только при диспетчеризации есть выигрыш у std::visit т.е. это пункт 3

На счет SRP.
На мой взгляд, наследование и вызов функций базового класса ни как не нарушают SRP.

Ну как правило наследование используется в качестве интерфейсов объектов

Наследование, это не только выделение интерфейсов. В целом можно сказать что это механизм для переиспользования кода, который в том числе (как вы и сказали) используется для выделения интерфейсов.
Если я вас правильно понял, вы хотите сделать вот это:
struct Object1
{
    void func() { /* */ }
};

struct Object2
{
    void func() {/* */}
};

struct ObjectInterface
{
    void func() { std::visit( [](auto&& obj) { obj->func(); }
    std::variant< Object1*, Object2* > var;
};

std::vector< ObjectInterface > vector;
for( auto& v : vector )
{
    v.func();
}


В таком случае, проблема не исчезнет (вы ее просто спрячите). Более того для Object1 и Object2 будут уже не доступны общие поля и функции-члены, а вот это уже плохо.

Дополню предыдущий комментарий, по производительности
std::visit выигрывает у данного подхода только при деспетчиризации в 1.2 раза.

По итогу:
1. Если есть не связанные между собой классы, то использование std::variant подходит лучше чем подход описанный в статье.
2. Если есть классы связанные между собой наследованием и через указатель на базовый класс достаются не только их настоящие типы, но и вызываются общие функцию-члены, то подход указанный в статье будет лучше.
(В любом случа, зависит от того, как часто вы вызываете те или иные функции ).

Здесь стоит перейти к вопросу о том, зачем нам наследование.
Наследование позволяет выделить общие функции-члены/поля для потомков.
Более того, можно ведь делать виртуальные функции-члены и в потомках их переопределять.

Вот только с std::variant есть проблема, вариант не наследует общего поведения.
Если у вас есть коллекция из указателей на базовый класс и вам необходимо
вызвать общую функцию-член, то проще и дешевле напрямую вызвать его имея на руках указатель на базовый класс, чем обходить std::variant с помощью std::visit.

Конечно, вы можете сделать вариант от всех видов потомков и с помощью
std::visit делать вызов метода, вот так:
std::vector< std::variant< Object1*, Object2* > > vector = { &t1, &e1, &t2, &t3, &e2, &e3, &t4 };
  
for( auto& v : vector )
{
    std::visit([](auto&& obj){ obj->test(); }, v);
}


Но, на мой взгляд такой код выглядет лучше:
std::vector< AbstractObject* > vector = { &t1, &e1, &t2, &t3, &e2, &e3, &t4 };
for( auto* obj : vector )
{
    obj->test();
}



Если сравнить производительность, то std::visit проигрывает.
Что стравнивалось:
1. вызов обычной функции-члена test с вызовом test через visit; (здесь visit почти в 5 раз проигрывает )
2. вызов виртуальной функции-члена virtualTest с вызовом virtualTest через visit;
3. вызов виртуальной функции-члена virutalTest с вызовом test через visit; (только в этом случае вызов через visit догнал виртуальный вызов, но не обогнал)
Вот тут код для quick bench.
Компилятор был выставлен Clang 10.0, std=c++17, optim=O3
Немного дополнил статью. Возможность пользоваться обычным посетителем остается, достаточно сделать своего наследника от AbstractVisitors .
Пример:
struct SomeVisitor : AbstractVisitors< ObjectList >
{
    void visit( Object1& obj ) override { /*some code*/ }
    void visit( Object2& obj ) override { /*some code*/ }
};

SomeVisitor some;
for( auto* obj : vector )
{
    obj->accept( some );
}
Да, здесь Dispatcher не имеет собственного стейта.
Механизм не может быть неинтрузивным, т.к. все типы, которые посетитель может обойти определены заранее в листе.

Если я вас правильно понял, вам нужно использовать какой-то уже имеющийся посетитель с внутренним стейтом, то можно сделать например вот так:

class Some
{
public:
    void visit( Object1& obj ) { /*some code*/}
    void visit( Object2& obj ) { /*some code*/}
private:
    int internalState;
};

Some some;
for( auto* obj : vector )
{
    obj->dispatch( [&some]( auto& obj ) { some.visit( obj ); } );
}

Да, действительно этот код не нужен, достаточно будет вот такого:
template< class ... >
struct Dispatcher;

Спасибо что заметили)
Да, можно и без листа, но тогда придется во всех методах accept писать весь список того, что посетитель может обойти (тут можно ошибиться и забыть что-нибудь). Если вам потребуется добавить/удалить класс из иерархии, то придется изменять все места, где присутствует эта последовательность. Лучше задавать этот список в одном месте и передавать куда необходимо.
Обычный посетитель сможет сделать все то же самое что и обобщенный.
Отличие будет только в том, что придется писать больше одинакового кода.

Приведу пример:
Положим у вас есть M абстрактных посетителей и N классов, которые эти посетители могут обойти.

Если вам необходимо вызывать шаблон функции, где аргумент шаблона будет типом, который будет скрываться за абстракцией, то вам необходимо будет реализовать MxN одинаковых функций visit у разных наследников от абстрактных посетителей.
Все реализации функции visit будут отличаться только тем, что будут иметь разный тип объекта.
template< class T>
void test()
{
}

struct AbstractVisitor1
{
    virtual void visit( Object1& obj ) = 0;
    virtual void visit( Object2& obj ) = 0;
};
struct AbstractVisitor2
{
    virtual void visit( Object3& obj ) = 0;
    virtual void visit( Object4& obj ) = 0;
};

struct Visitor1 : AbstractVisitor1
{
    void visit( Object1& obj ) override { test<Object1>(); }
    void visit( Object2& obj ) override { test<Object2>(); }
};

struct Visitor2 : AbstractVisitor2
{
    void visit( Object3& obj ) override { test<Object3>(); }
    void visit( Object4& obj ) override { test<Object4>(); }
};

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

Двойная диспетчеризация здесь присутствует так же как и у обычного посетителя.
1-й вызов функции-члена accept у AbstractObject внутри метода dispatch.
2-й вызов функции-члена visit у AbstractVisitor T.
После чего вызывается обобщенная лямда, где мы и получаем тип объекта, который можем использовать в шаблоне функции.
Отличие в том, что все это завернуто в функцию-член dispatch.

Information

Rating
Does not participate
Registered
Activity