Как стать автором
Обновить
8
0
Андрей Куприн @akuprin

Студент МГТУ им. Н. Э. Баумана

Отправить сообщение

Концепты - отличная штука, согласен, можно с их помощью доработать всё так, чтобы ошибки компиляции выглядели приятнее. А с помощью еще некоторых манипуляций (см. комментарии от пользователя @Kelbon) можно достичь той же гибкости в рантайме.

Дело в том, что мы не всегда можем иметь доступ к классу, который хотим завернуть в такую «типо-старательную» оболочку. Например, если мы хотим обращаться с похожими классами из разных библиотек, как с полиморфными. Классы библиотеки живут своей жизнью, а мы уже в своем коде можем определить функцию-обработчик (просто функцию, не метод класса) для каждого такого класса. Тем самым достигаем желаемого «полиморфизма» при этом не трогая библиотечный код.

Один из примеров из статьи про external polymorphism. Представьте себе, что мы хотим реализовать сериализацию для классов программы (и своих и библиотечных). Мы не можем взять и добавить им в иерархию общего предка с методом serialize(), тк библиотечные классы мы не можем менять. Но зато мы можем создать свою оболочку со стиранием типа для них и отдельно функции, которые уже умеют обрабатывать каждый из желаемых классов.

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

Ваша реализация тоже хороша, но она слегка другая. У вас стратегия на основе шаблонов. Соотвественно, так как шаблоны инстанцируются при компиляции, она не дает такой гибкости во время исполнения программы. Но с помощью нее тоже можно добиться интересных результатов.

А в моем примере я пишу, можно сказать «классическую» реализацию. Мы выносим вариативное поведение внутри методов класса в отдельную иерархию классов стратегий. Для создания такой иерархии мы первым делом определяем её базовый абстрактный класс. Отсюда и виртуальные методы, отсюда и указатели с полиморфизмом. А уже чтобы не пришлось возиться с временем жизни объектов, я использую умные указатели вместо обычных.

Так что все варианты имеют место быть, просто у каждого свое назначение(см. ссылка), свои плюсы и минусы.

Интересно, спасибо!

Иначе и городить ничего не надо, достаточно

Согласен. А то, что вы говорите, слегка упомянуто мной:

... Допустим, мы работаем с несколькими библиотеками и хотим написать сериализацию для объектов из этих библиотек. Взять и создать для них общий базовый класс не представляется возможным. ...

См. ссылка

Почему? Объясните, пожалуйста.

Как минимум, код становится красивее и читабельнее! А с std::variant могут возникнуть трудности, например: https://godbolt.org/z/633eh11rc

А в моём примере из класса фигуры выносится алгоритм её рисования. Считаю, что это всё-таки стратегия. Но прочитав ваш пример понял, что можно решить проблему и с помощью паттерна посетитель. Оба они в рамках данной задачи имеют место быть, просто говорить о них, считаю, нужно в разных местах.

Если идти по тем же шагам, что в статье. На тот момент, когда приводится решение проблемы с помощью стратегии, мы ещё не отказались от наличия метода draw в классах фигур.

Шаблон стратегия позволяет переключаться между алгоритмами (стратегиями) в зависимости от ситуации. Вот мы и пользуемся этим паттерном:

struct Circle : public Shape {
    std::unique_ptr<DrawStrategy> drawStrategy;
    explicit Circle(DrawStrategy *drawStrategy) 
        : drawStrategy(drawStrategy) { }
    void draw() override { drawStrategy->draw(this); }
};

Таким образом мы выделили в отдельный класс метод рисования.

С другой стороны: Шаблон посетитель позволяет добавлять будущие операции для объектов без их модифицирования. Вот на этапе, когда мы дошли до того, что в классах фигур больше нет метода draw(несколькими шагами позже примера со стратегией), думаю можно уже реализовывать визитора. Именно на этом моменте мы перестаём модифицировать классы, с которыми работаем, прямо как в кратком определении визитора, которое я привёл в начале абзаца.

И главное - мы их больше никогда не изменим!

В этот момент начинается рассказ о паттерне external polymorphism, поэтому визитор слегка отходит на второй план.

Да, я уже тогда понял, что есть разница, а я сам написал о другом, спасибо! И ваша библиотека, которая делает такое в рантайме, тоже отличная.

Насколько я понял пока изучал ассемблерный код примеров на godbolt.org, в этом деле нам очень сильно могут помочь отпимизации компилятора ("девиртуализация", кажется). (Не ручаюсь за свои слова здесь на 100%, я не эксперт ассемблера).

Как хорошо, что с недавнего времени у нас есть концепты. С ними ошибки намного приятнее читать)

И примеры использования:

Примеры взял отсюда (таймкод 49:00)

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;
}

Информация

В рейтинге
Не участвует
Откуда
Москва, Москва и Московская обл., Россия
Зарегистрирован
Активность

Специализация

Специалист
Git
SQL
Python
Linux
OOP
C++
C
Software development
C++ STL
Applied math