Search
Write a publication
Pull to refresh

Comments 33

Сказать что это страшно - ничего не сказать :) А я пишу на плюсах уже 8 лет)

За 8 лет можно бы CRTP и выучить уже

CRTP там в примере используется лишь как опциональная часть(чтобы получить метод в интерфейс), так что это реальный динамический полиморфизм, а не просто CRTP

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

template <typename T>
struct Print {
  static void do_invoke(const T& self) {
    std::cout << self << std::endl;
  }
};
using any_printable = aa::any_with<Print>;

void print_one(any_printable::const_ref p) {
  aa::invoke<Print>(p);
}

Вот так без CRTP это бы выглядело

Из пропущенного

  1. В Rust имеется статический вариант dyn Traits - impl Trait.

  2. Вариант enum dispatching в C++ так и просит обмазать его повсеместно constexpr.

Так ведь упомянул я про impl:
"В Rust статическая диспетчеризация реализуется с помощью типовых параметров или ключевого слова impl (по сути, являющего собой анонимные типовые параметры)."

Только примера с ним в статье так и не появилось.

А как же Java? Да, вы можете сказать что там дженериков нет, но в не можете сказать, что там нет полиморфизма!
Полиморфизм — он не шаблонами/джененриками едиными обеспечивается. Более того я бы даже сказал, что полиморфизм он вообще не про это. А дженерики, рефлекшены или просто банальные фабрики — это различные механизмы реализации данной концепции.
Так что — сабж в статье не раскрыт. Полиморфизм не подан — ни холодным, ни горячим. Кушать нечего!

А как же Java? Да, вы можете сказать что там дженериков нет, но в не можете сказать, что там нет полиморфизма!

А зачем здесь нужна Java? Тут показаны несколько видов реализации полиморфизма.
Автор статьи намекнул что ADHOC полиморфизм рассматривать не будет.
Следовательно, если брать Java, остаются два вида универсального полиморфизма:
параметрический и полиморфизм подтипов.
Оба они реализованы через динамическую диспетчеризацию а об этом в статье есть.

Просто статья именно про различные виды реализации полиморфизма а не про абстракциию.

Жду вопросы типа «а как же J?», «а как же BQN?», «а как же brainfuck?» :)

А как же вникнуть в смысл критики не цепляясь к первому предложению?

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

Не оскорбили, просто испортили о себе мнение. Я ж по делу. С добрыми намерениями. А вы даже понять ленитесь и/или объяснить. Ещё и требуете чтоб вас понимали. Не по Канту...

Вы, конечно, извините, но я совершенно не вижу добрых намерений в написании гневных комментариев до прочтения статьи. В статье представлены не только дженерики, про них только в разделе со статической диспетчеризацией. Далее представлены совершенно иные способы реализации полиморфизма. Отсутствие представления реализации на вашем любимом языке не означает отсутствия представления реализаций на других языках - очевидно, казалось бы, но вы путём отрицания данного ещё и пытаетесь меня заклеймить. Я бы сказал, что вы просто пришли со своим блюдом, дабы заявить, что оно лучше стряпни поваров ресторана - блюда вы своего, право, нам так и не показали (а мне лично не очень-то и хотелось).

Ладно, скажу грубо. Вы, недалёкий человек, неотличаете полиморфизм от механизмов его реализации. Не отличаете "шашечки" от "ехать". До вас даже не дошло, что джаву я упомянул только как пример, иллюстрацию моей основной претензии: что ваше название не соответсвует тексту статьи. От полиморфизма у вас в статье только названия двух его типов. А что, это, а как это, с чем его едят — у вас нет. Зато есть много, довольно хороших — мне понравилось, детальных разборов как это устроено в разных языковых средах. Но про сам полиморфизм, что собственно, я и ожидал, когда начинал читать эту статью — ни слова.


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


P.S. Нравится так общaться? Вот не общайтесь так. Будте вежливыми.

Так много понятнее. Заметить, право, должен, что помимо реализации в статье присутствует определение - чего вам ещё не хватает совершенно не понимаю, как, думаю, и вы сами. Если же нет, будьте добры чётко разъяснять, а не разбрасываться недо-оскорблениями.

"энамная диспетчеризация"...

Что за слово-то такое. Но это же примитивнейшая форма динамической диспетчеризации. И ни разу она не эффективней чем работа с таблицами виртуальных функций. В последней нет идиотского switch-case и прямо из таблицы за О(1) выбирается нужный вариант, и switch-case не нужно переписывать при добавлении нового типа.

Кроме того, не упомянута перегрузка функций, в C++ например. В этом смысле статическая диспетчеризация делается и без шаблонов. И вообще механизм ADL (argument dependent lookup), который позволяет выбрать в момент компиляции функцию в зависимости от типа аргумента.

Ключевое слово _Generic в голом C -- абсолютно бесполезная вещь в том применении, для чего его предлагалось использовать. Потому, что все возможные типы нужно заранее перечислить. Этот тот же switch-case но в момент компиляции. В итоге что только на этом _Generic не делают, у него масса применений, но для полиморфных функций он не пригоден практически вообще.

Без перечисления возможных типов вообще никуда не деться, но по крайней мере нужно только само перечисление, для получения некоторого порядкового номера, не более того. Декларировать функции и все подробности типа наперёд нужды нет, статический полиморфизм на голом C может быть реализован примерно следующим образом: https://www.onlinegdb.com/ncQeDOcPX (в одном файле: https://coliru.stacked-crooked.com/a/68011ec1308d8fe8).

Отмечу, что диспетчеризация времени компиляции случается только либо при компиляции в одном модуле, либо при использовании Link Time Optimization опции (-flto). Иначе всё скатывается обратно к диспетчеризации времени исполнения. Т.е. расчёт на то, что компилятор имеет возможность девиртуализировать вызовы (т.к. таблица виртуальных функций -- константа времени компиляции).

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

Ну, нужный case для перечисления тоже за O(1) выбирается обычно. И этот switch может запросто оказаться быстрее таблицы виртуальных функций за счёт большей локальности обращений к памяти.


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


Если нужно расширяться по операциям — типы-суммы эффективнее, если ожидается расширение по типам — эффективнее интерфейсы. Если ожидается расширение в обоих направлениях — поздравляю, вы прокляты.

При добавлении новых операций вовсе не обязательно "всё переписывать". В C++ может использоваться реализация по-умолчанию унаследованная от базового класса. Либо вовсе могут быть множественные интерфейсы и таблицы, тогда добавление нового интерфейса никак не затрагивает уже существующие объекты. И в конце концов добавить новую функцию в класс не проблема, тем более, что компилятор заставит так сделать для абстрактного базового класса.

А вот забыть добавить что-то в switch-case -- запросто. И никто не напомнит. Это сомнительная оптимизация. Я допускаю, что она выигрывает какие-то копейки с очень легковесными операциями в каком-то очень узком случае когда статическая диспетчеризация времени компиляции и/или развиртуализация ещё не срабатывают, а обращение к таблицам функций на сколько-то тактов тяжелее. Может быть в случае очень легковесных объектов, когда аж целый указатель на объект (для таблицы функций) -- дорого. Если не дорого, то наверное сделать ту же таблицу функций вручную, на шаблонах, и положить её куда-то в .text или выделенную секцию, если очень неймётся -- лучшее решение. И уж если так кроить, то можно не указатели хранить, а смещения относительно какой-то базы, там могут запросто быть 16-битные числа. Разница всё в том, что в варианте с switch-case содержимое таблицы контролируется только программистом, а в варианте с таблицами виртуальных функций компилятор не позволит что-то забыть (таблица может формироваться шаблоном из реально существующих функций классов, по заданному шаблону требующему наличия определённых функций с определёнными свойствами).

PS: хотя если типы -- это enum, то современные компиляторы умеют проверять, что в switch-case перебрали все возможные значения. Так что ошибку всё же можно задетектить.

А вот забыть добавить что-то в switch-case — запросто. И никто не напомнит.

Это смотря где.

Нужно наверное добавить какой смысл в статической диспетчеризации времени компиляции. Казалось бы бессмысленная вещь: чтоб применить к объекту функцию, нужно знать тип объекта. Но если мы знаем тип, то можем без всяких трюков прямо непосредсвенно вызвать нужную функцию... То ли дело динамическая диспетчеризация времени исполнения: там неизвестно какая ещё функция будет вызвана, и тип может быть неизвестно какой. Но из объекта достаются свойства его типа и вызывается нужная функция.

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

А к чему у автора относится golang:

package main

import (
	"bytes"
	"fmt"
	"io"
)

func Print[T io.Writer](b []byte, w T) error {
	_, err := w.Write(b)
	return err
}

func main() {
	buf := bytes.NewBuffer(nil)
	Print([]byte("12345"), buf)
	fmt.Println(buf.String())
}

К «энамной», словари ведь используются :)

Отлично, тогда "В качестве ограничителя также может выступать интерфейс any" у вас некорректно. Верно будет любой интерфейс или их объединение.

И где же, позвольте спросить, тут мьютекс?

Программисты умеют использовать полиморфизм и понимают что это такое. Я любитель и мое понимание полиморфизма очень искажено. Посему у меня вопрос, пожалуйста растолкуйте.

В примере динамического полиморфизма на с++ автор определяет сначала базовый класс

Display далее реализует два класса наследника DisplayInt и DisplayStr

Дальше реализует функцию display которая умеет создавать из int DisplayInt а из char* DisplayStr

Далее реализована функция print_dynamic которая запускает механизм writeln базового класса Display.

Как результат мы получили конструкцию вида :

print_dynamic(display(some_var));

НО у нас ведь была альтернатива реализовать две функции

print_dynamic(int); и print_dynamic(char*)

И случись изменения в формате отображения мы бы модифицировали эти функции, а не аналогичные DisplayInt::writeln DisplayStr::writeln.

По какой причине использованный подход лучше и удобнее подхода реализации в лоб.

Появись у нас еще тип нам все одно нужно будет либо создавать конвертер в имеющийся либо писать DisplaySomethingNew::writeln

Появись у нас новый функционал, скажем Display::write нам нужно будет пройтись по каждому классу и реализовать его.

Все это вопрос вкуса или все же за созданием такой структуры стоит определенная задача, которая дает некое преимущество ?

Он не лучше. Статья про (в некоторых местах с натяжкой) параметрический полиморфизм, то есть перегрузку функций мы отбрасываем. В данном конкретном случае можно было обойтись статическим полиморфизмом, описанным в первом пункте, но я также указал две разновидности динамического - не по причине того, что их стоит использовать в данной ситуации, но просто ради демонстрации.

К тому же, я не плюсовик, не пытайтесь перенять у меня какие, как их принято назвать, «паттерны», плохому научить могу, вместо этого просто смотрите на главную мысль и подход к реализации.

Жалко, как мне кажется подобные конструкции обладают красотой и хотелось бы понимать основания для их внедрения.

Полиморфизм (те его виды, которые перечислены в статье) пригождается когда требуется использовать некоторый общий код над разными типами.


Например, можно написать алгоритм вывода массива, и применить его как к массиву чисел, так и к массиву строк.


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


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

Да, в таком ключе мне тоже понятно как это работает. В принципе std::ostream& позволяет себя перегрузить. И куча функций в STL работает на этом принципе.

А в статье именно наследование классов с общим методом + перегрузкой функций.

Sign up to leave a comment.

Articles