Проблема с алмазом C ++ - как вызвать базовый метод только один раз

Я использую множественное наследование в C ++ и расширяю базовые методы, явно вызывая их базовые методы. Предположим следующую иерархию:

     Creature
    /        \
 Swimmer    Flier
    \        /
       Duck

Что соответствует

class Creature
{
    public:
        virtual void print()
        {
            std::cout << "I'm a creature" << std::endl;
        }
};

class Swimmer : public virtual Creature
{
     public:
        void print()
        {
            Creature::print();
            std::cout << "I can swim" << std::endl;
        }
};

class Flier : public virtual Creature
{
     public:
        void print()
        {
            Creature::print();
            std::cout << "I can fly" << std::endl;
        }
};

class Duck : public Flier, public Swimmer
{
     public:
        void print()
        {
            Flier::print();
            Swimmer::print();
            std::cout << "I'm a duck" << std::endl;
        }
};

Теперь это представляет проблему - вызов метода print утки вызывает соответствующие базовые методы, каждый из которых, в свою очередь, вызывает метод Creature::print(), поэтому в итоге он вызывается дважды -

I'm a creature
I can fly
I'm a creature
I can swim
I'm a duck

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

Есть ли какой-то встроенный способ сделать это или нам нужно прибегнуть к его реализации самостоятельно?

Если да, то как вы подойдете к этому?

Вопрос не только в печати. Мне было интересно, есть ли механизм для расширения базовых методов и функциональности, сохраняя порядок вызовов и избегая проблемы с ромбами.

Теперь я понимаю, что наиболее заметным решением было бы добавить вспомогательные методы, но я просто подумал, есть ли более «чистый» способ.


person O. Aroesti    schedule 24.04.2019    source источник
comment
Печать как Флиера, так и Пловца явно вызывает печать существа. На вашем месте я бы попытался решить проблему без этого наследования. Композиция превыше наследования. Например, ECS (система компонентов сущности) как раз собирается объединить свойства гибким способом без ада наследования.   -  person titapo    schedule 24.04.2019
comment
добавьте (защищенный) метод к вашему дополнительному std::cout, чтобы вы могли выбрать, какую именно версию вызывать.   -  person Jarod42    schedule 24.04.2019
comment
@titapo, это хорошее предложение, опубликуйте его как ответ, а не комментарий.   -  person Captain Man    schedule 24.04.2019
comment
Поскольку другие люди уже публиковали решения, я хотел бы здесь подчеркнуть, что вы вообще не хотите использовать множественное наследование, особенно наследование алмаза: stackoverflow.com/questions/406081/   -  person Lukas Plazovnik    schedule 24.04.2019
comment
Я всегда профессионально использую множественное наследование. Это неплохо, просто большинство людей не понимают последствий множественного наследования и того, как не зарыться в яму. Это замечательно для MVC и всевозможных графических интерфейсов!   -  person Wilfred Smith    schedule 25.04.2019
comment
Это плохо именно потому, что существует так много последствий и так легко зарыться в яму. Без него код будет чище и понятнее. Профессионально.   -  person Lightness Races in Orbit    schedule 25.04.2019


Ответы (7)


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

class Swimmer : public virtual Creature
{
     public:
        // Virtually inherit from Creature::print and extend it by another line of code
        void print() : virtual Creature::print()
        {
            std::cout << "I can swim" << std::endl;
        }
};

class Flier : public virtual Creature
{
     public:
        // Virtually inherit from Creature::print and extend it by another line of code
        void print() : virtual Creature::print()
        {
            std::cout << "I can fly" << std::endl;
        }
};

class Duck : public Flier, public Swimmer
{
     public:
        // Inherit from both prints. As they were created using "virtual function inheritance",
        // this will "mix" them just like in virtual class inheritance
        void print() : Flier::print(), Swimmer::print()
        {
            std::cout << "I'm a duck" << std::endl;
        }
};

Итак, ответ на ваш вопрос

Есть ли какой-нибудь встроенный способ сделать это?

равно нет. Ничего подобного в C ++ не существует. Кроме того, я не знаю ни одного другого языка, в котором есть что-то подобное. Но это интересная идея ...

person sebrockm    schedule 25.04.2019

Скорее всего, это проблема XY. Но ... только не называй это дважды.

#include <iostream>

class Creature
{
public:
    virtual void identify()
    {
        std::cout << "I'm a creature" << std::endl;
    }
};

class Swimmer : public virtual Creature
{
public:
    virtual void identify() override
    {
        Creature::identify();
        tell_ability();
        std::cout << "I'm a swimmer\n";
    }

    virtual void tell_ability()
    {
        std::cout << "I can swim\n";
    }
};

class Flier : public virtual Creature
{
public:
    virtual void identify() override
    {
        Creature::identify();
        tell_ability();
        std::cout << "I'm a flier\n";
    }

    virtual void tell_ability()
    {
        std::cout << "I can fly\n";
    }
};

class Duck : public Flier, public Swimmer
{
public:
    virtual void tell_ability() override
    {
        Flier::tell_ability();
        Swimmer::tell_ability();
    }

    virtual void identify() override
    {
        Creature::identify();
        tell_ability();
        std::cout << "I'm a duck\n";
    }
};

int main()
{
    Creature c;
    c.identify();
    std::cout << "------------------\n";

    Swimmer s;
    s.identify();
    std::cout << "------------------\n";

    Flier f;
    f.identify();
    std::cout << "------------------\n";

    Duck d;
    d.identify();
    std::cout << "------------------\n";
}

Вывод:

I'm a creature
------------------
I'm a creature
I can swim
I'm a swimmer
------------------
I'm a creature
I can fly
I'm a flier
------------------
I'm a creature
I can fly
I can swim
I'm a duck
------------------
person Swordfish    schedule 24.04.2019

Мы можем позволить базовому классу отслеживать атрибуты:

#include <iostream>
#include <string>
#include <vector>

using namespace std::string_literals;

class Creature
{
public:
    std::string const attribute{"I'm a creature"s};
    std::vector<std::string> attributes{attribute};
    virtual void print()
    {
        for (auto& i : attributes)
            std::cout << i << std::endl;
    }
};

class Swimmer : public virtual Creature
{
public:
    Swimmer() { attributes.push_back(attribute); }
    std::string const attribute{"I can swim"s};
};

class Flier : public virtual Creature
{
public:
    Flier() { attributes.push_back(attribute); }
    std::string const attribute{"I can fly"s};
};

class Duck : public Flier, public Swimmer
{
public:
    Duck() { attributes.push_back(attribute); }
    std::string const attribute{"I'm a duck"s};
};

int main()
{
    Duck d;
    d.print();
}

Точно так же, если нам нужна не просто печать, а вызовы функций, то мы могли бы позволить базовому классу отслеживать функции:

#include <iostream>
#include <functional>
#include <vector>

class Creature
{
public:
    std::vector<std::function<void()>> print_functions{[this] {Creature::print_this(); }};
    virtual void print_this()
    {
        std::cout << "I'm a creature" << std::endl;
    }
    void print()
    {
        for (auto& f : print_functions)
            f();
    }
};

class Swimmer : public virtual Creature
{
public:
    Swimmer() { print_functions.push_back([this] {Swimmer::print_this(); }); }
    void print_this()
    {
        std::cout << "I can swim" << std::endl;
    }
};

class Flier : public virtual Creature
{
public:
    Flier() { print_functions.push_back([this] {Flier::print_this(); }); }
    void print_this()
    {
        std::cout << "I can fly" << std::endl;
    }
};

class Duck : public Flier, public Swimmer
{
public:
    Duck() { print_functions.push_back([this] {Duck::print_this(); }); }
    void print_this()
    {
        std::cout << "I'm a duck" << std::endl;
    }
};

int main()
{
    Duck d;
    d.print();
}
person wally    schedule 24.04.2019
comment
Это заставляет каждый экземпляр выделять весь std::vector, что немного тяжеловато. - person Kevin; 24.04.2019
comment
@Kevin Думаю, тебе тоже не понравятся vtables. :) - person wally; 25.04.2019
comment
@wally vtables подразумевает хранение указателя на бит постоянной памяти - часто это можно сделать с помощью одной инструкции перемещения. Векторы включают выделение памяти из кучи, что предполагает захват и освобождение мьютекса со всеми вытекающими отсюда барьерами памяти и очисткой кеша. - person Martin Bonner supports Monica; 25.04.2019
comment
@MartinBonner Другие потоки не будут иметь доступа к объекту до завершения построения. Зачем вам нужно блокировать мьютекс для вектора? А где вы узнали, что вам нужен мьютекс для выделения кучи? - person wally; 25.04.2019
comment
@wally - это выделение в куче, которому нужен мьютекс. Куча - это общая структура данных во всех реализациях, с которыми я знаком, поэтому для нее нужен мьютекс. (Вы можете выделить из кучи для каждого потока, но вам нужно поддерживать освобождение из произвольного потока, поэтому вам все равно нужен мьютекс.) - person Martin Bonner supports Monica; 25.04.2019
comment
@MartinBonner Если вы имеете в виду блокировку, которую делает операционная система, то это неизбежно. Я полагаю, что если вы создадите вектор с правильным размером, дополнительный malloc может быть оптимизирован, если вы все равно создаете весь объект в куче. Конкуренция между потоками также может быть уменьшена, если ОС решит использовать отдельные области для распределения. ОС также может не использовать мьютекс. В общем, оборонительное проектирование вокруг этого было бы непрактичным. std :: vector не так уж и плох. - person wally; 25.04.2019

Простой способ - создать группу вспомогательных классов, которые имитируют структуру наследования вашей основной иерархии и выполняют всю печать в своих конструкторах.

 struct CreaturePrinter {
    CreaturePrinter() { 
       std::cout << "I'm a creature\n";
    }
 };

 struct FlierPrinter: virtual CreaturePrinter ... 
 struct SwimmerPrinter: virtual CreaturePrinter ...
 struct DuckPrinter: FlierPrinter, SwimmerPrinter ...

Затем каждый метод печати в основной иерархии просто создает соответствующий вспомогательный класс. Нет ручного связывания.

Для удобства обслуживания вы можете сделать каждый класс принтера вложенным в соответствующий основной класс.

Естественно, в большинстве реальных случаев вы хотите передать ссылку на основной объект в качестве аргумента конструктору его помощника.

person n. 1.8e9-where's-my-share m.    schedule 24.04.2019

Ваши явные вызовы print методов составляют суть проблемы.

Один из способов обойти это - отбросить вызовы print и заменить их, скажем,

void queue(std::set<std::string>& data)

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

Затем вы реализуете печать набора одним методом в Creature.

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

person Bathsheba    schedule 24.04.2019

Если вам нужен этот метод среднего класса, не вызывайте метод базового класса. Самый простой и легкий способ - извлечь дополнительные методы, а затем легко реализовать Print.

class Creature
{
    public:
        virtual void print()
        {
            std::cout << "I'm a creature" << std::endl;
        }
};

class Swimmer : public virtual Creature
{
     public:
        void print()
        {
            Creature::print();
            detailPrint();
        }

        void detailPrint()
        {
            std::cout << "I can swim" << std::endl;
        }
};

class Flier : public virtual Creature
{
     public:
        void print()
        {
            Creature::print();
            detailPrint();
        }

        void detailPrint()
        {
            std::cout << "I can fly" << std::endl;
        }
};

class Duck : public Flier, public Swimmer
{
     public:
        void print()
        {
            Creature::Print();
            Flier::detailPrint();
            Swimmer::detailPrint();
            detailPrint();
        }

        void detailPrint()
        {
            std::cout << "I'm a duck" << std::endl;
        }
};

Без подробностей, в чем заключается ваша настоящая проблема, трудно придумать лучшее решение.

person Marek R    schedule 24.04.2019

Использовать:

template<typename Base, typename Derived>
bool is_dominant_descendant(Derived * x) {
    return std::abs(
        std::distance(
            static_cast<char*>(static_cast<void*>(x)),
            static_cast<char*>(static_cast<void*>(dynamic_cast<Base*>(x)))
        )
    ) <= sizeof(Derived);
};

class Creature
{
public:
    virtual void print()
    {
        std::cout << "I'm a creature" << std::endl;
    }
};

class Walker : public virtual Creature
{
public:
    void print()
    {
        if (is_dominant_descendant<Creature>(this))
            Creature::print();
        std::cout << "I can walk" << std::endl;
    }
};

class Swimmer : public virtual Creature
{
public:
    void print()
    {
        if (is_dominant_descendant<Creature>(this))
            Creature::print();
        std::cout << "I can swim" << std::endl;
    }
};

class Flier : public virtual Creature
{
public:
    void print()
    {
        if (is_dominant_descendant<Creature>(this))
            Creature::print();
        std::cout << "I can fly" << std::endl;
    }
};

class Duck : public Flier, public Swimmer, public Walker
{
public:
    void print()
    {
        Walker::print();
        Swimmer::print();
        Flier::print();
        std::cout << "I'm a duck" << std::endl;
    }
};

А с Visual Studio 2015 вывод будет следующим:

I'm a creature
I can walk
I can swim
I can fly
I'm a duck

Но is_dominant_descendant не имеет переносимого определения. Хотелось бы, чтобы это была стандартная концепция.

person Red.Wave    schedule 24.04.2019