Концепты - отличная штука, согласен, можно с их помощью доработать всё так, чтобы ошибки компиляции выглядели приятнее. А с помощью еще некоторых манипуляций (см. комментарии от пользователя @Kelbon) можно достичь той же гибкости в рантайме.
Дело в том, что мы не всегда можем иметь доступ к классу, который хотим завернуть в такую «типо-старательную» оболочку. Например, если мы хотим обращаться с похожими классами из разных библиотек, как с полиморфными. Классы библиотеки живут своей жизнью, а мы уже в своем коде можем определить функцию-обработчик (просто функцию, не метод класса) для каждого такого класса. Тем самым достигаем желаемого «полиморфизма» при этом не трогая библиотечный код.
Один из примеров из статьи про external polymorphism. Представьте себе, что мы хотим реализовать сериализацию для классов программы (и своих и библиотечных). Мы не можем взять и добавить им в иерархию общего предка с методом serialize(), тк библиотечные классы мы не можем менять. Но зато мы можем создать свою оболочку со стиранием типа для них и отдельно функции, которые уже умеют обрабатывать каждый из желаемых классов.
В общем суть в том, что функции обработчики находятся вне самих обрабатываемых классов по тем или иным причинам. А данный паттерн позволяет не смотря на это связать их и заставить работать вместе так, как будто классы полиморфные.
Ваша реализация тоже хороша, но она слегка другая. У вас стратегия на основе шаблонов. Соотвественно, так как шаблоны инстанцируются при компиляции, она не дает такой гибкости во время исполнения программы. Но с помощью нее тоже можно добиться интересных результатов.
А в моем примере я пишу, можно сказать «классическую» реализацию. Мы выносим вариативное поведение внутри методов класса в отдельную иерархию классов стратегий. Для создания такой иерархии мы первым делом определяем её базовый абстрактный класс. Отсюда и виртуальные методы, отсюда и указатели с полиморфизмом. А уже чтобы не пришлось возиться с временем жизни объектов, я использую умные указатели вместо обычных.
Так что все варианты имеют место быть, просто у каждого свое назначение(см. ссылка), свои плюсы и минусы.
Согласен. А то, что вы говорите, слегка упомянуто мной:
... Допустим, мы работаем с несколькими библиотеками и хотим написать сериализацию для объектов из этих библиотек. Взять и создать для них общий базовый класс не представляется возможным. ...
А в моём примере из класса фигуры выносится алгоритм её рисования. Считаю, что это всё-таки стратегия. Но прочитав ваш пример понял, что можно решить проблему и с помощью паттерна посетитель. Оба они в рамках данной задачи имеют место быть, просто говорить о них, считаю, нужно в разных местах.
Если идти по тем же шагам, что в статье. На тот момент, когда приводится решение проблемы с помощью стратегии, мы ещё не отказались от наличия метода draw в классах фигур.
Шаблон стратегия позволяет переключаться между алгоритмами (стратегиями) в зависимости от ситуации. Вот мы и пользуемся этим паттерном:
Таким образом мы выделили в отдельный класс метод рисования.
С другой стороны: Шаблон посетитель позволяет добавлять будущие операции для объектов без их модифицирования. Вот на этапе, когда мы дошли до того, что в классах фигур больше нет метода draw(несколькими шагами позже примера со стратегией), думаю можно уже реализовывать визитора. Именно на этом моменте мы перестаём модифицировать классы, с которыми работаем, прямо как в кратком определении визитора, которое я привёл в начале абзаца.
И главное - мы их больше никогда не изменим!
В этот момент начинается рассказ о паттерне external polymorphism, поэтому визитор слегка отходит на второй план.
Насколько я понял пока изучал ассемблерный код примеров на godbolt.org, в этом деле нам очень сильно могут помочь отпимизации компилятора ("девиртуализация", кажется). (Не ручаюсь за свои слова здесь на 100%, я не эксперт ассемблера).
std::variant основан на фиксированном наборе типов и предоставляет открытый набор операций над ними. Мы же пытаемся достигнуть обратного - открытого для расширения набора типов и фиксированного набора операций над ними.
А вот в Си такое можно и средствами языка (_Generic) начиная с C11, посмотрите:
#include <stdio.h>
typedef struct {} spaceship;
typedef struct {} star;
typedef struct {} asteroid;
void ship_asteroid(spaceship s, asteroid a){printf("ship_asteroid\n");}
void ship_star(spaceship s, star){printf("ship_star\n");}
void star_star(star a, star b){printf("star_star\n");}
void ship_ship(spaceship a, spaceship b){printf("ship_ship\n");}
#define dispatch(x, y) \
_Generic((x), \
spaceship: _Generic((y), \
asteroid : ship_asteroid, \
star : ship_star, \
spaceship : ship_ship), \
star : star_star)(x, y)
int main(int argc, char *argv[]) {
spaceship sh;
asteroid as;
star st;
dispatch(sh, as);
dispatch(sh, st);
dispatch(st, st);
dispatch(sh, sh);
return 0;
}
Вывод ожидаемый:
ship_asteroid
ship_star
star_star
ship_ship
К сожалению, если типов сделать больше и ко всем еще добавлять функции, принимающие их, то из этого выйдет мешанина. Но базово сделать все-таки что-то можно!
И не очень понятно, раз мы говорим про C++, то чем плоха специализация шаблонов?
#include <iostream>
#include <string>
struct spaceship {};
struct asteroid {};
struct star {};
template <typename A, typename B> void dispatch(A a, B b) {
static_assert(false, "specialization required");
}
template <> void dispatch<spaceship, asteroid>(spaceship a, asteroid b) {
std::cout << "spaceship, asteroid" << std::endl;
}
template <> void dispatch<spaceship, star>(spaceship a, star b) {
std::cout << "spaceship, star" << std::endl;
}
template <> void dispatch<star, star>(star a, star b) {
std::cout << "star, star" << std::endl;
}
template <> void dispatch<spaceship, spaceship>(spaceship a, spaceship b) {
std::cout << "spaceship, spaceship" << std::endl;
}
int main(int argc, char *argv[]) {
spaceship sh;
asteroid as;
star st;
dispatch(1, 2); // ошибка компиляции
dispatch(sh, as);
dispatch(sh, st);
dispatch(st, st);
dispatch(sh, sh);
return 0;
}
Концепты - отличная штука, согласен, можно с их помощью доработать всё так, чтобы ошибки компиляции выглядели приятнее. А с помощью еще некоторых манипуляций (см. комментарии от пользователя @Kelbon) можно достичь той же гибкости в рантайме.
Дело в том, что мы не всегда можем иметь доступ к классу, который хотим завернуть в такую «типо-старательную» оболочку. Например, если мы хотим обращаться с похожими классами из разных библиотек, как с полиморфными. Классы библиотеки живут своей жизнью, а мы уже в своем коде можем определить функцию-обработчик (просто функцию, не метод класса) для каждого такого класса. Тем самым достигаем желаемого «полиморфизма» при этом не трогая библиотечный код.
Один из примеров из статьи про external polymorphism. Представьте себе, что мы хотим реализовать сериализацию для классов программы (и своих и библиотечных). Мы не можем взять и добавить им в иерархию общего предка с методом serialize(), тк библиотечные классы мы не можем менять. Но зато мы можем создать свою оболочку со стиранием типа для них и отдельно функции, которые уже умеют обрабатывать каждый из желаемых классов.
В общем суть в том, что функции обработчики находятся вне самих обрабатываемых классов по тем или иным причинам. А данный паттерн позволяет не смотря на это связать их и заставить работать вместе так, как будто классы полиморфные.
Ваша реализация тоже хороша, но она слегка другая. У вас стратегия на основе шаблонов. Соотвественно, так как шаблоны инстанцируются при компиляции, она не дает такой гибкости во время исполнения программы. Но с помощью нее тоже можно добиться интересных результатов.
А в моем примере я пишу, можно сказать «классическую» реализацию. Мы выносим вариативное поведение внутри методов класса в отдельную иерархию классов стратегий. Для создания такой иерархии мы первым делом определяем её базовый абстрактный класс. Отсюда и виртуальные методы, отсюда и указатели с полиморфизмом. А уже чтобы не пришлось возиться с временем жизни объектов, я использую умные указатели вместо обычных.
Так что все варианты имеют место быть, просто у каждого свое назначение(см. ссылка), свои плюсы и минусы.
Интересно, спасибо!
Согласен. А то, что вы говорите, слегка упомянуто мной:
См. ссылка
Почему? Объясните, пожалуйста.
Как минимум, код становится красивее и читабельнее! А с
std::variant
могут возникнуть трудности, например: https://godbolt.org/z/633eh11rcА в моём примере из класса фигуры выносится алгоритм её рисования. Считаю, что это всё-таки стратегия. Но прочитав ваш пример понял, что можно решить проблему и с помощью паттерна посетитель. Оба они в рамках данной задачи имеют место быть, просто говорить о них, считаю, нужно в разных местах.
Если идти по тем же шагам, что в статье. На тот момент, когда приводится решение проблемы с помощью стратегии, мы ещё не отказались от наличия метода
draw
в классах фигур.Шаблон стратегия позволяет переключаться между алгоритмами (стратегиями) в зависимости от ситуации. Вот мы и пользуемся этим паттерном:
С другой стороны: Шаблон посетитель позволяет добавлять будущие операции для объектов без их модифицирования. Вот на этапе, когда мы дошли до того, что в классах фигур больше нет метода
draw
(несколькими шагами позже примера со стратегией), думаю можно уже реализовывать визитора. Именно на этом моменте мы перестаём модифицировать классы, с которыми работаем, прямо как в кратком определении визитора, которое я привёл в начале абзаца.В этот момент начинается рассказ о паттерне external polymorphism, поэтому визитор слегка отходит на второй план.
Да, я уже тогда понял, что есть разница, а я сам написал о другом, спасибо! И ваша библиотека, которая делает такое в рантайме, тоже отличная.
Насколько я понял пока изучал ассемблерный код примеров на godbolt.org, в этом деле нам очень сильно могут помочь отпимизации компилятора ("девиртуализация", кажется). (Не ручаюсь за свои слова здесь на 100%, я не эксперт ассемблера).
Как хорошо, что с недавнего времени у нас есть концепты. С ними ошибки намного приятнее читать)
И примеры использования:
https://github.com/thecppzoo/zoo
https://github.com/ldionne/dyno
Boost Type Erasure
Примеры взял отсюда (таймкод 49:00)
А вот в Си такое можно и средствами языка (_Generic) начиная с C11, посмотрите:
Вывод ожидаемый:
К сожалению, если типов сделать больше и ко всем еще добавлять функции, принимающие их, то из этого выйдет мешанина. Но базово сделать все-таки что-то можно!
И не очень понятно, раз мы говорим про C++, то чем плоха специализация шаблонов?