Здравствуйте, so5team, Вы писали:
BFE>>В отличии от статического и динамического полиморфизма, полиморфизм основанный на Signals and slots является параметрическим полиморфизм S>А чем статический полиморфизм отличается от параметрического?
Тем, что при статическом полиморфизме вызывающий знает тип вызываемого объекта, при параметрическом — нет.
Здравствуйте, B0FEE664, Вы писали:
BFE>>>В отличии от статического и динамического полиморфизма, полиморфизм основанный на Signals and slots является параметрическим полиморфизм S>>А чем статический полиморфизм отличается от параметрического?
BFE>Тем, что при статическом полиморфизме вызывающий знает тип вызываемого объекта, при параметрическом — нет.
Боюсь, здесь две проблемы с моим восприятием вашего определения:
1. Там, где "знает тип", там полиморфизм, если и есть, то разве что ad-hoc. Который в C++ реализуется посредством перегрузки функций под разные типы параметров (вроде to_string(int) и to_string(double)).
2. Параметрическим полиморфизмом принято называть то, что в C++ называется шаблонами. Т.е. когда алгоритмы и/или структуры данных оперируют не конкретными типами (аргументов, атрибутов), а именами этих типов. Т.е. вот это уже параметрический полиморфизм:
template< typename T > T min(T a, T b);
Соответственно, в C++ параметрический полиморфизм используется для реализации альтернативы динамическому полиморфизму. Т.е. вместо:
// Классический ООП с динамическим полиморфизмом.class Provider {
public:
virtual void f() = 0;
...
};
class ConsumerOne {
Provider & provider_;
...
void g() { provider_.f(); }
};
class ConsumerTwo {
Provider & provider_;
...
void g() { provider_.f(); }
};
class ActualProviderOne : public Provider {...};
class ActualProviderTwo : public Provider {...};
ActualProviderOne provider_one;
ConsumerOne consumer_one{provider_one};
consumer_one.g();
ActualProviderTwo provider_two;
ConsumerTwo consumer_two{provider_two};
consumer_two.g();
можно использовать другой сценарий:
// Альтернатива на базе параметрического полиморфизма.template< typename ProviderT >
class ConsumerOne {
ProviderT & provider_;
...
void g() { provider_.f(); }
};
template< typename ProviderT >
class ConsumerTwo {
ProviderT & provider_;
...
void g() { provider_.f(); }
};
class ActualProviderOne {...};
class ActualProviderTwo {...};
ActualProviderOne provider_one;
ConsumerOne<ActualProviderOne> consumer_one{provider_one};
consumer_one.g();
ActualProviderTwo provider_two;
ConsumerTwo<ActualProviderTwo> consumer_two{provider_two};
consumer_two.g();
И этот альтернативный подход в C++ называют "статическим полиморфизмом". По крайней мере я в таком ключе этот термин использовал.
А вот что вы подразумеваете под статическим и параметрическим полиморфизмом не очень понятно
Здравствуйте, so5team, Вы писали: S>Боюсь, здесь две проблемы с моим восприятием вашего определения:
Об определениях спорить не буду, просто покажу ещё одну альтернативу.
Скрытый текст
S>1. Там, где "знает тип", там полиморфизм, если и есть, то разве что ad-hoc. Который в C++ реализуется посредством перегрузки функций под разные типы параметров (вроде to_string(int) и to_string(double)).
S>2. Параметрическим полиморфизмом принято называть то, что в C++ называется шаблонами. Т.е. когда алгоритмы и/или структуры данных оперируют не конкретными типами (аргументов, атрибутов), а именами этих типов. Т.е. вот это уже параметрический полиморфизм: S>
template< typename T > T min(T a, T b);
S>Соответственно, в C++ параметрический полиморфизм используется для реализации альтернативы динамическому полиморфизму. Т.е. вместо:
Ниже ConsumerOne и ConsumerTwo знают о Provider S>
S>И этот альтернативный подход в C++ называют "статическим полиморфизмом". По крайней мере я в таком ключе этот термин использовал.
Ниже ConsumerOne и ConsumerTwo ничего не знают ни о ActualProviderOne, ни о ActualProviderTwo (все 4 класса могут лежать в разных файлах и не include'ить друг друга):
BFE>Ниже ConsumerOne и ConsumerTwo ничего не знают ни о ActualProviderOne, ни о ActualProviderTwo (все 4 класса могут лежать в разных файлах и не include'ить друг друга): BFE>
Да это вроде обычный динамический полиморфизм, только реализуется он у вас через идиомы из функционального программирования. Но, по сути, это всего лишь что-то вроде:
BFE>Значит, придется делать фиктивный базовый класс для Module, Function и т.д.
BFE>Вот как так-то?
Там в статье то ли в погоне за простотой, то ли в погоне за статьей вообще потерялся смысл. Ведь в итоге напихали функции в какой-то адаптер и никакой такой унификации то по сути нет. Получилось из простого и понятного дерева — дерево вывернутое через шаблоны с совершенно наркоманскими суффиксами.
Ну т.е., если у предлагаемого подхода и есть польза, то что бы полностью осознать преимущества, видимо прийдется вникать в реальный код.
Здравствуйте, B0FEE664, Вы писали:
BFE>Т.е. сделать run шаблонным виртуальным методом.
Тема пять лет как набивающая оскомину? шаблонный виртуальный метод
S>Да это вроде обычный динамический полиморфизм, только реализуется он у вас через идиомы из функционального программирования. Но, по сути, это всего лишь что-то вроде: S>А std::function с лямбдами в C++ можно рассматривать как сахар для классов вроде ProviderCaller/CallerForProviderOne/CallerForProviderTwo.
Есть принципиальная разница состоящая в том, что ConsumerOne и ConsumerTwo ничего не знают о ProviderCaller. Это не просто сахар, это принципиально другой вид отношений между классами. (По сути такой код ближе к тому определению ООП в котором объекты обмениваются сообщениями.) В случае использования Signals-slots классы можно переименовывать и менять независимо друг от друга, а в приведённом примере изменения названия ProviderCaller, скажем на ProxyCaller приведёт к редактированию аж 4-х классов!
Есть и другое отличие.
Если вам нужно будет добавить провайдера для второй функции (с такой же сигнатурой):
Здравствуйте, a7d3, Вы писали:
BFE>>Т.е. сделать run шаблонным виртуальным методом. A>Тема пять лет как набивающая оскомину? шаблонный виртуальный метод
Да. Хотя теме больше пяти лет.
Здравствуйте, B0FEE664, Вы писали:
S>>А std::function с лямбдами в C++ можно рассматривать как сахар для классов вроде ProviderCaller/CallerForProviderOne/CallerForProviderTwo.
BFE>Есть принципиальная разница состоящая в том, что ConsumerOne и ConsumerTwo ничего не знают о ProviderCaller.
Зато они вынуждены знать про std::function<void()>. Тоже самое, что и знать про ProviderCaller с operator()
BFE>Это не просто сахар, это принципиально другой вид отношений между классами.
Пока не видно почему.
BFE>В случае использования Signals-slots классы можно переименовывать и менять независимо друг от друга, а в приведённом примере изменения названия ProviderCaller, скажем на ProxyCaller приведёт к редактированию аж 4-х классов!
Тоже самое произойдет если вы решите заменить std::function на какой-нибудь my_fast_delegate.
Ну и, как уже было сказано, в случае с лямбдами у вас есть сахар со стороны компилятора, который автоматически делает классы, который мне пришлось выписывать руками.
Но с точки зрения типа применяющегося здесь полиморфизма разницы нет.
BFE>Есть и другое отличие. BFE>Если вам нужно будет добавить провайдера для второй функции (с такой же сигнатурой): BFE>
Здравствуйте, so5team, Вы писали:
S>Ну и, как уже было сказано, в случае с лямбдами у вас есть сахар со стороны компилятора, который автоматически делает классы, который мне пришлось выписывать руками.
S>Но с точки зрения типа применяющегося здесь полиморфизма разницы нет.
Одно дело разница в механике реализации со стороны компилятора — механизмах взаимодействия объектов (экземпляров классов).
Совсем другое — это разница в плане характера взаимоотношений сущностей порождаемых разработчиком (программистом) в исходном коде и которые приводят к кодогенерации во время компиляции.
Последнее относится к тому пространству, где есть понятие связность и хрупкость исходного кода какого-то программного решения. В аспекте управления жизненным циклом — внесением изменений по части корректировки или доработки реализованного функционала.
Здравствуйте, a7d3, Вы писали:
PM>>Насколько я понял, это просто список функций, вызываемых последовательно, одна за другой. Зачем-то его обзвали Manager
PM>>Так можно написать на любом языке, где функции являются полноценными гражданами, хоть на Haskell, хоть на JavaScript. Ну и на C++ само собой.
A>std::function & lambda появились в плюсах «недавно» и в результате увлечения функторами, вместо указателей на «свободные функции».
boost::function уже лет 20, а лямбда так это вообще синтаксический оверхедсахар над
Ну и boost.lambda для любителей странного существовала примерно с тех же времен.
Кто хотел — использовал такое, кто не знал — использовал классический ОО подход с интерфейсами и динамическим полиморфизмом.
Не понятно, какое отношение имеет код из стартового сообщения к "Concept-Based Polymorphism". Если я правильно помню, то последнее это попытка перенести идею type classes из Haskell.
Один из таких экспериментов на С++ недавно было https://github.com/ldionne/dyno
Но как-то тоже неочевидна практическая цель применения этого.
A>А обозвали manager, скорее всего, из-за возможности добавлять-удалять проходы. Вряд ли из-за древовидной структуры произвольной вложенности, которые у них лепятся из этих манагеров-прохода.
Я неясно высказался. Бессмысленный суффикс в имени класса, типа Manager, Controller, Container, Coordiantor, Holder и т.п. обычно является признаком того, что автор, возможно, сам не до конца понимает зачем такой класс нужен.
Здравствуйте, night beast, Вы писали:
BFE>>Ещё одно отличие в том, что для Signals-slots использование виртуальных функций не обязательно. NB>как без них?
Здравствуйте, B0FEE664, Вы писали:
BFE>>>Ещё одно отличие в том, что для Signals-slots использование виртуальных функций не обязательно. NB>>как без них?
BFE>Как в приведённом выше примере.
Здравствуйте, so5team, Вы писали:
S>>>А std::function с лямбдами в C++ можно рассматривать как сахар для классов вроде ProviderCaller/CallerForProviderOne/CallerForProviderTwo. BFE>>Есть принципиальная разница состоящая в том, что ConsumerOne и ConsumerTwo ничего не знают о ProviderCaller. S>Зато они вынуждены знать про std::function<void()>. Тоже самое, что и знать про ProviderCaller с operator()
Нет, не тоже самое. От std::function<void()> никто не наследуется.
BFE>>Это не просто сахар, это принципиально другой вид отношений между классами. S>Пока не видно почему.
Взгляните на архитектуру примеров. У них разная архитектура.
BFE>>В случае использования Signals-slots классы можно переименовывать и менять независимо друг от друга, а в приведённом примере изменения названия ProviderCaller, скажем на ProxyCaller приведёт к редактированию аж 4-х классов! S>Тоже самое произойдет если вы решите заменить std::function на какой-нибудь my_fast_delegate.
Нет конечно. В ConsumerOne можно заменить std::function на какой-нибудь my_fast_delegate, а в ConsumerTwo оставить как есть.
S>Ну и, как уже было сказано, в случае с лямбдами у вас есть сахар со стороны компилятора, который автоматически делает классы, который мне пришлось выписывать руками.
Не согласен. В std::function есть стиратель типа, а у вас его нет. Вот если бы вы предложили заменить std::function указателем на функцию, тогда можно было бы согласится.
S>Но с точки зрения типа применяющегося здесь полиморфизма разницы нет.
Т.е. вы хотите поговорить об определениях.
Существует несколько разновидностей полиморфизма. Две принципиально различных из них были описаны Кристофером Стрэчи[en] в 1967 году: это параметрический полиморфизм[⇨] и ad-hoc-полиморфизм[⇨], причём первая является истинной формой, а вторая — мнимой[1][4]; прочие формы являются их подвидами или сочетаниями.
Ну так какой это полиморфизм?
BFE>>Есть и другое отличие. BFE>>Если вам нужно будет добавить провайдера для второй функции (с такой же сигнатурой): BFE>>
BFE>>то как вы будете это реализовывать в CallerForProviderOne ? S>Не понятно, при чем здесь CallerForProviderOne?
Ну а как ? Заведёте ещё CallerForProviderThree? И так для каждого нового метода?
BFE>>Ещё одно отличие в том, что для Signals-slots использование виртуальных функций не обязательно. S>Напомню, что речь шла о различиях между статическим и параметрическим полиморфизмом. Точнее в выяснении того, что вы под этим понимаете.
Ну раз вам так интересно, что подразумеваю я (быть может не правильно), то вот:
В случае использования шаблонных параметров — статический полиморфизм.
В случае использования виртуальных функций — динамический полиморфизм.
В случае использования type eraser — параметрический полиморфизм.
Здравствуйте, night beast, Вы писали:
BFE>>>>Ещё одно отличие в том, что для Signals-slots использование виртуальных функций не обязательно. NB>>>как без них? BFE>>Как в приведённом выше примере.
Какое отношение внутренняя реализация std::function имеет отношение к обсуждаемой теме?
А, впрочем, вот, для ConsumerThree без использования std::function:
Здравствуйте, B0FEE664, Вы писали:
S>>>>А std::function с лямбдами в C++ можно рассматривать как сахар для классов вроде ProviderCaller/CallerForProviderOne/CallerForProviderTwo. BFE>>>Есть принципиальная разница состоящая в том, что ConsumerOne и ConsumerTwo ничего не знают о ProviderCaller. S>>Зато они вынуждены знать про std::function<void()>. Тоже самое, что и знать про ProviderCaller с operator() BFE>Нет, не тоже самое. От std::function<void()> никто не наследуется.
Это не важно. Важно то, что Consumer-ы должны иметь какую-то сущность, которую в данном случае логично было бы назвать "слот". Через "слот" Consumer получает возможность вызвать Provider-а.
Поскольку в C++ каждая сущность должна быть представлена экземпляром какого-то типа, то для представления "слота" нужно выбрать подходящий тип. В вашем примере это std::function<void()>, в моем примере -- абстрактный класс ProviderCaller.
Но суть от этого не меняется, т.к. Consumer-ы хранят в себе "слоты" через которые происходит вызов.
BFE>Взгляните на архитектуру примеров. У них разная архитектура.
Не вижу разной архитектуры. Вижу различия в реализациях, но архитектура как раз практически эквивалентная.
BFE>>>В случае использования Signals-slots классы можно переименовывать и менять независимо друг от друга, а в приведённом примере изменения названия ProviderCaller, скажем на ProxyCaller приведёт к редактированию аж 4-х классов! S>>Тоже самое произойдет если вы решите заменить std::function на какой-нибудь my_fast_delegate. BFE>Нет конечно. В ConsumerOne можно заменить std::function на какой-нибудь my_fast_delegate, а в ConsumerTwo оставить как есть.
Тогда что мешает вам в ConsumerOne заменть ProviderCaller на ProxyCaller, а в ConsumerTwo оставить ProviderCaller?
Еще раз подчеркну: суть в том, что идею "слота" вам в программе нужно через что-то выразить. Либо через std::function, либо через my_fast_delegate, либо через ProviderCaller. И после того, как вы выразили "слот" тем или иным образом, замена реализации "слота" потребует модификации мест использования слота. Вне зависимости от того, как именно "слот" реализован.
S>>Ну и, как уже было сказано, в случае с лямбдами у вас есть сахар со стороны компилятора, который автоматически делает классы, который мне пришлось выписывать руками. BFE>Не согласен. В std::function есть стиратель типа, а у вас его нет. Вот если бы вы предложили заменить std::function указателем на функцию, тогда можно было бы согласится.
Не вижу какой-либо существенной роли "стирателя типа" здесь.
S>>Но с точки зрения типа применяющегося здесь полиморфизма разницы нет. BFE>Т.е. вы хотите поговорить об определениях.
Не поговорить, а договорится.
BFE>Ну так какой это полиморфизм?
Я не понял, этот вопрос относится к цитате из русской Wiki или?
Вообще, за определениями лучше заглядывать в англоязычную Wiki, ее больше читают и вероятность, что оттуда вымарают разную ересь повыше: https://en.wikipedia.org/wiki/Polymorphism_(computer_science)
BFE>>>то как вы будете это реализовывать в CallerForProviderOne ? S>>Не понятно, при чем здесь CallerForProviderOne? BFE>Ну а как ? Заведёте ещё CallerForProviderThree? И так для каждого нового метода?
Послушайте, мне не понятен сам вопрос. Но вы вместо того, чтобы его уточнить, задаете следующий вопрос, что озадачивает меня еще больше.
Подозреваю, что вас интересует ситуация следующего рода:
И вы хотите, чтобы в ConsumerOne можно было вызывать как ActualProviderTwo::Function_f, так и ActualProviderTwo::Function_g.
Если вопрос именно в этом, то да, будет еще один класс CallerForProviderTwoG.
Только тут нужно подчеркнуть важную вещь: я специально привожу примеры в классическом ООП-стиле без использования обобщенного программирования. Поэтому получается, что под каждого Provider-а нужно вручную делать собственного наследника от ProviderCaller. Делаю это для того, чтобы показать, что точно такой же результат, как у вас, достигается исключительно посредством динамического полиморфизма.
Откуда можно сделать вывод, что то, что вы называете "параметрическим полиморфизмом" есть не что иное, как динамический полиморфизм, вид в профиль.
BFE>Ну раз вам так интересно, что подразумеваю я (быть может не правильно), то вот: BFE>В случае использования шаблонных параметров — статический полиморфизм. BFE>В случае использования виртуальных функций — динамический полиморфизм. BFE>В случае использования type eraser — параметрический полиморфизм.
Понятно. Но такое определение параметрического полиморфизма кардинально противоречит тому, к чему я привык. И, судя по определениям из Wiki, привык не только я.
Здравствуйте, so5team, Вы писали:
BFE>>>>Есть принципиальная разница состоящая в том, что ConsumerOne и ConsumerTwo ничего не знают о ProviderCaller. S>>>Зато они вынуждены знать про std::function<void()>. Тоже самое, что и знать про ProviderCaller с operator() BFE>>Нет, не тоже самое. От std::function<void()> никто не наследуется. S>Это не важно. Важно то, что Consumer-ы должны иметь какую-то сущность, которую в данном случае логично было бы назвать "слот". Через "слот" Consumer получает возможность вызвать Provider-а.
Структура слота также важна, так как от устройства слота зависит сложность его использования.
S>Поскольку в C++ каждая сущность должна быть представлена экземпляром какого-то типа, то для представления "слота" нужно выбрать подходящий тип. В вашем примере это std::function<void()>, в моем примере -- абстрактный класс ProviderCaller. S>Но суть от этого не меняется, т.к. Consumer-ы хранят в себе "слоты" через которые происходит вызов.
Смотря что называть сутью. Если сутью называть наличие полиморфизма, то он есть в обоих случаях, а если сутью называть "степень связности классов", т.е. эта степень различна.
BFE>>Взгляните на архитектуру примеров. У них разная архитектура. S>Не вижу разной архитектуры. Вижу различия в реализациях, но архитектура как раз практически эквивалентная.
Ну как она может быть одинакова, когда в вашем случае есть наследование, а у меня его нет?
BFE>>>>В случае использования Signals-slots классы можно переименовывать и менять независимо друг от друга, а в приведённом примере изменения названия ProviderCaller, скажем на ProxyCaller приведёт к редактированию аж 4-х классов! S>>>Тоже самое произойдет если вы решите заменить std::function на какой-нибудь my_fast_delegate. BFE>>Нет конечно. В ConsumerOne можно заменить std::function на какой-нибудь my_fast_delegate, а в ConsumerTwo оставить как есть. S>Тогда что мешает вам в ConsumerOne заменть ProviderCaller на ProxyCaller, а в ConsumerTwo оставить ProviderCaller?
Вы путаете, я говорю про то, что изменение названия, переименование ProviderCaller в ProxyCaller, приведёт к изменениям, а не про замену одного другим.
S>Еще раз подчеркну: суть в том, что идею "слота" вам в программе нужно через что-то выразить. Либо через std::function, либо через my_fast_delegate, либо через ProviderCaller. И после того, как вы выразили "слот" тем или иным образом, замена реализации "слота" потребует модификации мест использования слота. Вне зависимости от того, как именно "слот" реализован.
Конечно потребуется изменение в точке связывания, но суть совершенно не в этом, а в том, что в моём случае добавление ещё одного слота не ведёт к написанию ещё одного класса, как это сделано у вас. Классы CallerForProviderOne и CallerForProviderTwo не нужны. Их не должно существовать. Это лишние сущности не выражающие ничего существенного.
S>>>Ну и, как уже было сказано, в случае с лямбдами у вас есть сахар со стороны компилятора, который автоматически делает классы, который мне пришлось выписывать руками. BFE>>Не согласен. В std::function есть стиратель типа, а у вас его нет. Вот если бы вы предложили заменить std::function указателем на функцию, тогда можно было бы согласится. S>Не вижу какой-либо существенной роли "стирателя типа" здесь.
А она есть и важна. Если у нас есть стиратель типа, то один и тот же код мы вызываем для неважно каких типов, так как информацию о типе мы стёрли и не используем. Посмотрите на пример для языка C:
В языке Си функции не являются объектами первого класса, но возможно определение указателей на функции, что позволяет строить функции высших порядков[96]. Также доступен небезопасный параметрический полиморфизм за счёт явной передачи необходимых свойств типа через бестиповое подмножество языка, представленное нетипизированным указателем void*[97] (называемым в сообществе языка «обобщённым указателем» (англ. generic pointer). Назначение и удаление информации о типе при приведении типа к void* и обратно не является ad-hoc-полиморфизмом, так как не меняет представление указателя, однако, его записывают явно для обхода системы типов компилятора
В C++ тоже можно стирать тип и тоже можно это делать различными способами. Стирание типа приводит к настоящему параметрическому полиморфизму, так как мы получаем возможность выполнять один и тот же код для любых типов.
BFE>>Ну так какой это полиморфизм? S>Я не понял, этот вопрос относится к цитате из русской Wiki или?
Какой тип полиморфизма в приведённом мною примере?
S>Вообще, за определениями лучше заглядывать в англоязычную Wiki, ее больше читают и вероятность, что оттуда вымарают разную ересь повыше: https://en.wikipedia.org/wiki/Polymorphism_(computer_science)
Упоминание слова ересь тут к чему?
Ну возьмём английское определение:
Parametric polymorphism: when one or more types are not specified by name but by abstract symbols that can represent any type.
std::function<void()> — это абстрактный символ, который может представлять любой тип. Согласны?
S>Только тут нужно подчеркнуть важную вещь: я специально привожу примеры в классическом ООП-стиле без использования обобщенного программирования. Поэтому получается, что под каждого Provider-а нужно вручную делать собственного наследника от ProviderCaller. Делаю это для того, чтобы показать, что точно такой же результат, как у вас, достигается исключительно посредством динамического полиморфизма.
Ещё раз повторюсь, конечно результат тот же — и там и там полиморфизм. Дело не в результате, а в том, с помощью каких средств он достигается.
S>Откуда можно сделать вывод, что то, что вы называете "параметрическим полиморфизмом" есть не что иное, как динамический полиморфизм, вид в профиль.
Если под динамическим полиморфизмом понимать такой полиморфизм, который может изменятся во время выполнения программы, то — да. И тот и другой являются динамическими. Но полиморфизм основанный на Signal-Slot ещё и параметрический.
BFE>>Ну раз вам так интересно, что подразумеваю я (быть может не правильно), то вот: BFE>>В случае использования шаблонных параметров — статический полиморфизм. BFE>>В случае использования виртуальных функций — динамический полиморфизм. BFE>>В случае использования type eraser — параметрический полиморфизм. S>Понятно. Но такое определение параметрического полиморфизма кардинально противоречит тому, к чему я привык. И, судя по определениям из Wiki, привык не только я.
В чём отличие?
Здравствуйте, B0FEE664, Вы писали:
BFE>>>>>Ещё одно отличие в том, что для Signals-slots использование виртуальных функций не обязательно. NB>>>>как без них? BFE>>>Как в приведённом выше примере.
NB>>виртуальная функция внутри std::function
BFE>Какое отношение внутренняя реализация std::function имеет отношение к обсуждаемой теме?
если обсуждается стирание типов, то мне непонятно, как оно будет работать без виртуальных функций или их ручной замены в виде указателей на функцию.
то есть то что у тебя наружу не торчит какой-то интерфейс, не значит что его совсем нет.
Здравствуйте, B0FEE664, Вы писали:
BFE>Структура слота также важна, так как от устройства слота зависит сложность его использования.
При выяснении того, какой тип полиморфизма вы подразумевали, устройство слота принципиального значения не имеет.
BFE>Смотря что называть сутью. Если сутью называть наличие полиморфизма, то он есть в обоих случаях, а если сутью называть "степень связности классов", т.е. эта степень различна.
Сутью в этом обсуждении я называю тип полиморфизма, который в вашей формулировке обозначен как "параметрический".
BFE>>>Взгляните на архитектуру примеров. У них разная архитектура. S>>Не вижу разной архитектуры. Вижу различия в реализациях, но архитектура как раз практически эквивалентная. BFE>Ну как она может быть одинакова, когда в вашем случае есть наследование, а у меня его нет?
Между Consumer-ами и Provider-ами никакого наследования нет. И прямой связи между ними нет как в моем, так и в вашем примере. Поэтому архитектура, в которую входят Consumer-ы и Provider-ы принципиально не различается.
А вот архитектура самого механизма слотов -- да, разная.
Но. Я специально показал вам реализацию слотов на базе наследования. Чтобы более четко показать, что здесь имеет место быть обычный динамический полиморфизм.
А так-то слоты можно было и без наследования сделать. Вы сами где-то в этой теме вариант с указателями на функцию уже это показали.
BFE>Вы путаете, я говорю про то, что изменение названия, переименование ProviderCaller в ProxyCaller, приведёт к изменениям, а не про замену одного другим.
Ну так если комитет примет решения переименовать std::function в std::functor, например, то и у вас будет та же самая проблема.
BFE>Конечно потребуется изменение в точке связывания, но суть совершенно не в этом, а в том, что в моём случае добавление ещё одного слота не ведёт к написанию ещё одного класса, как это сделано у вас. Классы CallerForProviderOne и CallerForProviderTwo не нужны. Их не должно существовать. Это лишние сущности не выражающие ничего существенного.
См. выше про причины того, почему эти классы вообще появились.
BFE>А она есть и важна.
К сожалению, ваша аргументация не выглядит убедительно.
BFE>В C++ тоже можно стирать тип и тоже можно это делать различными способами. Стирание типа приводит к настоящему параметрическому полиморфизму, так как мы получаем возможность выполнять один и тот же код для любых типов.
У меня есть впечатление, что вы неправильно понимаете "параметрический полиморфизм". А так же не правильно интерпретируете "возможность выполнять один и тот же код для любых типов".
BFE>Какой тип полиморфизма в приведённом мною примере?
Динамический. Я об этом уже вроде бы говорил.
BFE>std::function<void()> — это абстрактный символ, который может представлять любой тип. Согласны?
Нет. Это конкретный тип, который обозначает функцию без аргументов, возвращающую void. И ваше решение завязано конкретно на этот тип.
Если вы смените его на тип void(int) или void(int, string), то работать оно уже не будет.
BFE>Ещё раз повторюсь, конечно результат тот же — и там и там полиморфизм. Дело не в результате, а в том, с помощью каких средств он достигается.
Нет, еще раз повторюсь. Дело не в средствах, а в типе полиморфизма. Это динамический полиморфизм. А реализуется он разными (на первый взгляд) средствами.
BFE>>>Ну раз вам так интересно, что подразумеваю я (быть может не правильно), то вот: BFE>>>В случае использования шаблонных параметров — статический полиморфизм. BFE>>>В случае использования виртуальных функций — динамический полиморфизм. BFE>>>В случае использования type eraser — параметрический полиморфизм. S>>Понятно. Но такое определение параметрического полиморфизма кардинально противоречит тому, к чему я привык. И, судя по определениям из Wiki, привык не только я. BFE>В чём отличие?
Вот пример параметрического полиморфизма:
template< typename T > T min(T a, T b) { return a < b ? a : b; }
Это код, который будет работать с разными типами T: int, float, complex, string, vector<char> и т.д.
А вот то, что вы считаете параметрическим полиморфизмом:
Вы так считаете, поскольку wrap_action получает разные лямбды.
Но дело в том, что с точки зрения типов обе эти лямбды имеет одинаковый тип (точнее, у них одинаковый тип после оборачивания в std::function или после приведения к void(*)()). Поэтому wrap_action заточен только под один тип. И с другим типом параметра он работать не сможет. Поэтому это не параметрический полиморфизм. ИМХО.