Определить общий интерфейс без динамического полиморфизма

В таких приложениях, как трассировка лучей, объект может относиться к одному из нескольких типов, имеющих общий интерфейс. Например, 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? А именно, определите тип, который может быть одним из нескольких типов. Было бы неплохо заставить программиста определить собственную реализацию общего поведения для каждого подтипа, но это не обязательно.

Одной из альтернатив динамическому полиморфизму с виртуальными функциями является использование std::variant : не требуется ни базовый класс, ни указатель на базовый класс, ни виртуальные функции; «определить тип, который может быть одним из нескольких типов» — это именно то, что сделал std::variant. Однако я не думаю, что снижение производительности виртуальной функции очень велико.

jls28 01.09.2024 20:04

@ jls28 jls28 Это было бы очень похоже на обычный полиморфизм во время выполнения с использованием функций virtual, но без преимуществ. Поскольку классы реализуют несколько интерфейсов, количество комбинаций вскоре станет неуправляемым. ОП хочет, чтобы проблема была решена во время компиляции

Ted Lyngmo 01.09.2024 20:51

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

jls28 01.09.2024 21:08

@jls28 Jls28 Просто потому, что класс имеет виртуальные функции, он не требует автоматического выделения динамической памяти, хотя это обычное явление. Я не понимаю, почему определение интерфейса в базовом классе и/или наличие указателя базового класса — это плохо, когда требуется полиморфизм во время выполнения. Виртуальная таблица, скорее всего, будет более эффективной, чем std::visitvariant, особенно вариант, который должен охватывать огромное количество комбинаций интерфейса. В любом случае, как я уже упоминал, OP хочет полиморфизма времени компиляции, поэтому ни полиморфные классы, ни std::variant не подходят.

Ted Lyngmo 01.09.2024 21:34
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
4
64
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Вы можете создать 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++? Возможно, какой-то непонятный шаблон проектирования? Если я правильно помню, можно использовать «Любопытно повторяющийся шаблон шаблона».

Kotaka Danski 02.09.2024 07:41

@KotakaDanski Добро пожаловать! До появления концепций можно было создавать признаки типа для выполнения аналогичных проверок. CRTP также может быть полезен как до C++20, так и после него.

Ted Lyngmo 02.09.2024 07:56

решение с вариантом:

#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>, даже несмотря на то, что он занимает больше памяти в кеше.

Kotaka Danski 02.09.2024 08:07

Другие вопросы по теме