Проблема с алмазом в C ++ - Как вызвать базовый метод только один раз - PullRequest
37 голосов
/ 24 апреля 2019

Я использую множественное наследование в 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

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

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

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

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

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

Ответы [ 7 ]

50 голосов
/ 24 апреля 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
------------------
22 голосов
/ 24 апреля 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();
}
8 голосов
/ 24 апреля 2019

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

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

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

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

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

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

5 голосов
/ 24 апреля 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;
        }
};

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

5 голосов
/ 24 апреля 2019

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

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

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

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

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

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

4 голосов
/ 24 апреля 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 не имеет переносимого определения.Хотелось бы, чтобы это была стандартная концепция.

2 голосов
/ 25 апреля 2019

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

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 ++. Кроме того, я не знаю ни о каком другом языке, который имеет что-то подобное. Но это интересная идея ...

...