В таких приложениях, как трассировка лучей, объект может относиться к одному из нескольких типов, имеющих общий интерфейс. Например, Material
может быть DiffuseMaterial
или ReflectiveMaterial
и т. д., и все они поддерживают метод Color getColor(args);
. В большинстве приложений эта проблема обычно решается с помощью динамического полиморфизма:
class Material {
public:
virtual Color getColor() = 0;
// ...
}
class DiffuseMaterial: public Material {
public:
Color getColor() {
// ...
}
}
class ReflectiveMaterial: public Material {
public:
Color getColor() {
// ...
}
}
Затем функция может использовать Material*
для представления материала любого из этих типов.
Эта иерархия виртуальных классов не только позволяет пользователю определять общее поведение, но и заставляет пользователя определять виртуальную функцию, иначе программист не сможет создавать экземпляры производных классов, поскольку они абстрактны. Однако динамический полиморфизм приводит к снижению производительности из-за динамической отправки и поиска. Это проблема высокопроизводительных программ.
Более современные языки предоставляют альтернативу динамическому полиморфизму. Например, в Rust есть trait
(интерфейсы, которые можно разрешить во время компиляции) и enum
(объекты которых могут быть структурами), которые позволяют избежать динамического полиморфизма. Например,
enum Material {
DiffuseMaterial{member1, member2}, // a struct
ReflectiveMaterial {member1, member2, member3, } // a struct
}
Как добиться того же эффекта динамического полиморфизма, используя функции полиморфизма времени компиляции в C++20? А именно, определите тип, который может быть одним из нескольких типов. Было бы неплохо заставить программиста определить собственную реализацию общего поведения для каждого подтипа, но это не обязательно.
@ jls28 jls28 Это было бы очень похоже на обычный полиморфизм во время выполнения с использованием функций virtual
, но без преимуществ. Поскольку классы реализуют несколько интерфейсов, количество комбинаций вскоре станет неуправляемым. ОП хочет, чтобы проблема была решена во время компиляции
Я не согласен, есть преимущества: не требуется динамическая память, нет базового класса, нет указателя на базовый класс.
@jls28 Jls28 Просто потому, что класс имеет виртуальные функции, он не требует автоматического выделения динамической памяти, хотя это обычное явление. Я не понимаю, почему определение интерфейса в базовом классе и/или наличие указателя базового класса — это плохо, когда требуется полиморфизм во время выполнения. Виртуальная таблица, скорее всего, будет более эффективной, чем std::visit
variant
, особенно вариант, который должен охватывать огромное количество комбинаций интерфейса. В любом случае, как я уже упоминал, OP хочет полиморфизма времени компиляции, поэтому ни полиморфные классы, ни std::variant
не подходят.
Вы можете создать concept
, который проверит все ваши требования к Material
:
template <class T>
concept IMaterial = requires(T m) {
// list the requirements here
{ m.getColor() } -> std::same_as<Color>;
};
Фактические классы, реализующие требования, могут быть совершенно не связаны между собой:
class DiffuseMaterial {
public:
Color getColor() { return Color{}; }
};
class ReflectiveMaterial {
public:
Color getColor() { return Color{}; }
};
... и вы можете использовать concept
, чтобы принимать только те типы, которые соответствуют требованиям.
Пример:
template<IMaterial M> // M must fulfill the requirements
class Foo : public M { // or composition if that makes more sense
void func() {
Color c = this->getColor();
}
};
Это отлично сработало бы, спасибо! Это просто, а не уродливо и запутанно. Я также нашел эту статью, в которой предоставляется дополнительная помощь. Интересно, какова была практика до появления концепций C++? Возможно, какой-то непонятный шаблон проектирования? Если я правильно помню, можно использовать «Любопытно повторяющийся шаблон шаблона».
@KotakaDanski Добро пожаловать! До появления концепций можно было создавать признаки типа для выполнения аналогичных проверок. CRTP также может быть полезен как до C++20, так и после него.
решение с вариантом:
#include <variant>
enum Color {}; // what you wish
class DiffuseMaterial {
public:
Color getColor() { return Color{}; }
};
class ReflectiveMaterial {
public:
Color getColor() { return Color{}; }
};
void your_function(std::variant<DiffuseMaterial, ReflectiveMaterial> & mat)
{
Color color = std::visit([](auto & m) {return m.getColor();}, mat);
}
int main()
{
std::variant<DiffuseMaterial, ReflectiveMaterial> mat = DiffuseMaterial{};
your_function(mat);
}
Спасибо за эту альтернативу. Определение типа для std::variant<...>
должно сделать это еще более кратким. Однако, судя по тому, что я читал о std::variant
, для std::visit
используется динамическая диспетчеризация, что может снизить возможный выигрыш. Некоторые утверждают, что это можно оптимизировать, но это не всегда гарантировано. Думаю, мне придется протестировать и посмотреть, что работает лучше всего. Например, для представления различных видов лучей использование члена перечисления RayType
оказалось быстрее, чем Ray<RayType>
, даже несмотря на то, что он занимает больше памяти в кеше.
Одной из альтернатив динамическому полиморфизму с виртуальными функциями является использование std::variant : не требуется ни базовый класс, ни указатель на базовый класс, ни виртуальные функции; «определить тип, который может быть одним из нескольких типов» — это именно то, что сделал std::variant. Однако я не думаю, что снижение производительности виртуальной функции очень велико.