Проблема с алмазом в С++ - Как вызвать базовый метод только один раз
Я использую множественное наследование в 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;
}
};
Теперь это создает проблему - вызов метода duck print
вызывает соответствующие базовые методы, каждый из которых, в свою очередь, вызывает метод Creature::print()
, поэтому в итоге он вызывается twice-
I'm a creature
I can fly
I'm a creature
I can swim
I'm a duck
Я хотел бы найти способ убедиться, что базовый метод вызывается только один раз. Нечто подобное тому, как работает виртуальное наследование (вызов базового конструктора при первом вызове, а затем только присвоение ему указателя при последующих вызовах из других производных классов).
Есть ли какой-то встроенный способ сделать это или нам нужно прибегнуть к его реализации самостоятельно?
Если так, как бы вы подошли к этому?
Вопрос не относится к печати. Я задавался вопросом, существует ли механизм для расширения базовых методов и функциональности, сохраняя при этом порядок вызовов и избегая проблемы с бриллиантами.
Теперь я понимаю, что наиболее выдающимся решением было бы добавление вспомогательных методов, но мне просто было интересно, есть ли "более чистый" способ.
Ответы
Ответ 1
Скорее всего, это проблема 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
------------------
Ответ 2
Мы можем позволить базовому классу отслеживать атрибуты:
#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();
}
Ответ 3
Самый простой способ - создать группу вспомогательных классов, которые имитируют структуру наследования вашей основной иерархии и выполняют всю печать в их конструкторах.
struct CreaturePrinter {
CreaturePrinter() {
std::cout << "I'm a creature\n";
}
};
struct FlierPrinter: virtual CreaturePrinter ...
struct SwimmerPrinter: virtual CreaturePrinter ...
struct DuckPrinter: FlierPrinter, SwimmerPrinter ...
Затем каждый метод печати в основной иерархии просто создает соответствующий вспомогательный класс. Нет ручной цепочки.
Для удобства обслуживания вы можете сделать каждый класс принтера вложенным в соответствующий ему основной класс.
Естественно, в большинстве случаев вы хотите передать ссылку на главный объект в качестве аргумента конструктору его помощника.
Ответ 4
Ваши явные обращения к методам print
составляют суть проблемы.
Одним из способов решения этой проблемы было бы сбросить вызовы для print
и заменить их, скажем,
void queue(std::set<std::string>& data)
и вы накапливаете сообщения печати в set
. Тогда не имеет значения, что эти функции в иерархии вызываются более одного раза.
Затем вы реализуете печать набора одним способом в Creature
.
Если вы хотите сохранить порядок печати, вам нужно заменить set
другим контейнером, который учитывает порядок вставки и отклоняет дубликаты.
Ответ 5
Если вы хотите этот метод среднего класса, не вызывайте метод базового класса. Самый простой и простой способ - извлечь дополнительные методы, а затем легко переопределить 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;
}
};
Без подробностей, какова ваша настоящая проблема, трудно найти лучшее решение.
Ответ 6
Использование:
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
не имеет переносимого определения. Я хотел бы, чтобы это была стандартная концепция.
Ответ 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++. Кроме того, я не знаю ни о каком другом языке, который имеет что-то подобное. Но это интересная идея...
Ответ 8
Поскольку другие люди уже публиковали решения, я хотел бы подчеркнуть здесь, что вы на самом деле не хотите использовать множественное наследование и особенно наследование алмазов вообще:
Почему я должен избегать множественного наследования в C++?