Кто о чём, а я снова о сабже.
Доколе?
В смысле: сколько ещё ждать?
Когда, наконец, добавят в стандарт?
Скептикам и тем, кому нужны virtual template methods представляю работающую реализацию.
Идея проста, пишем в базовом классе шаблонный метод virtual_fun, который вызывает шаблонную функцию CallFunImpl добавив первым параметром this.
Шаблонная функция CallFunImpl представляет из себя "virtual method table" (на самом деле просто switch по id класса) и вызывает соответствующий перегруженный метод virtual_fun_ для каждого из класса.
Нет ничего сложного в том, чтобы компилятор сгенерил CallFunImpl для каждого виртуального шаблонного метода, но писателем компиляторов что-то мешает. Кто-нибудь знает, что именно им мешает?
#include <iostream>
class A;
template <class T>
void CallFunImpl(A* p, T t);
class A
{
public:
template <class T>
void virtual_fun(T t)
{
CallFunImpl(this, t);
}
virtual int GetThisId()
{
return 0;
}
template <class T>
void virtual_fun_(T t)
{
std::cout << "A:: t is " << t << std::endl;
}
};
class B : public A
{
public:
virtual int GetThisId()
{
return 1;
}
template <class T>
void virtual_fun_(T t)
{
std::cout << "B:: t is " << t << std::endl;
}
};
class C : public A
{
public:
virtual int GetThisId()
{
return 2;
}
};
class D : public A
{
public:
virtual int GetThisId()
{
return 3;
}
template <class T>
void virtual_fun_(T t)
{
std::cout << "D:: t is " << t << std::endl;
}
};
class E : public D
{
public:
virtual int GetThisId()
{
return 4;
}
template <class T>
void virtual_fun_(T t)
{
std::cout << "E:: t is " << t << std::endl;
}
};
template <class T>
void CallFunImpl(A* p, T t)
{
switch(p->GetThisId())
{
case 0: p ->A::virtual_fun_(t); break;
case 1: static_cast<B*>(p)->B::virtual_fun_(t); break;
case 2: static_cast<C*>(p)->C::virtual_fun_(t); break;
case 3: static_cast<D*>(p)->D::virtual_fun_(t); break;
case 4: static_cast<E*>(p)->E::virtual_fun_(t); break;
}
}
int main(int argc, char* argv[])
{
A* arr[] = {new A, new B, new C, new D, new E};
for(A* p : arr)
{
p->virtual_fun(1);
p->virtual_fun('c');
p->virtual_fun(1.1f);
}
E e;
e.virtual_fun("I am virtual template function!");
B b;
b.virtual_fun("I am virtual template function!");
return 0;
}
BFE>Нет ничего сложного в том, чтобы компилятор сгенерил CallFunImpl для каждого виртуального шаблонного метода, но писателем компиляторов что-то мешает. Кто-нибудь знает, что именно им мешает?
Так раздельная компиляция же. Пока вся программа умещается в одном файле, действительно, нет ничего сложного. Как только появляется пара объектных файлов, то внезапно не получается в реализации базового класса перечислить всех его наследников и все используемые в них шаблонные специализации, о которых базовый класс на момент компиляции ничего не знает. В варианте без шаблонов просто создаётся vtable со всеми возможными сигнатурами уже известных виртуальных функций, а наследники её используют. В случае же шаблонов что и сколько класть в такую vtable неизвестно пока не получен список всех наследников и всех используемых специализаций. И даже техники вроде whole program optimization такой список построить не помогут — ведь наследник может объявится где-нибудь в сторонней dll или so.
BFE>В смысле: сколько ещё ждать? BFE>Когда, наконец, добавят в стандарт?
Так экспорт шаблонов наоборот убрали из стандарта. А ждать шаблонные виртуальные методы раньше возврата экспорта ну явно не стоит.
8.1 Virtual Function Templates
Virtual function templates are a powerful abstraction mechanismnot part of C++. Generating v-tables for virtual function templates requires a whole-program view and C++ traditionally relies almost exclusively on separate compilation of translation units. The prelinker technique described here should be able to synthesize v-tables for virtual function templates as it does for open-methods.
Здравствуйте, watchmaker, Вы писали:
BFE>>Нет ничего сложного в том, чтобы компилятор сгенерил CallFunImpl для каждого виртуального шаблонного метода, но писателем компиляторов что-то мешает. Кто-нибудь знает, что именно им мешает? W>Так раздельная компиляция же. Пока вся программа умещается в одном файле, действительно, нет ничего сложного. Как только появляется пара объектных файлов, то внезапно не получается в реализации базового класса перечислить всех его наследников и все используемые в них шаблонные специализации, о которых базовый класс на момент компиляции ничего не знает. В варианте без шаблонов просто создаётся vtable со всеми возможными сигнатурами уже известных виртуальных функций, а наследники её используют. В случае же шаблонов что и сколько класть в такую vtable неизвестно пока не получен список всех наследников и всех используемых специализаций. И даже техники вроде whole program optimization такой список построить не помогут — ведь наследник может объявится где-нибудь в сторонней dll или so.
Всё это похоже на отговорки, потому, что:
1. приведённый код можно разбросать по многим файлам и он будет работать (и компилироваться, и линковаться). При этом внешняя шаблонная функция, заменяющая таблицу вызовов для виртуальных шаблонных методов, может быть создана кодогенератором (это сложно, но никакой интеллектуальной работы требующей участия человека не требуется)
2. реализация вызова виртуальных методов не обязана базироваться на единой таблице — стандарт этого не требует. Нет ничего страшного в том, чтобы под каждый шаблонный виртуальный метод добавить свою таблицу вызовов для всех специализаций этой функции. Да, все возможные сигнатуры не известны в одном модуле, но что мешает написать сбор всех сигнатур для специализаций этой функции из всех модулей? Сейчас шаблонные функции тоже разбросаны по разным модулям и что? Ведь все шаблонные функции с одинаковой сигнатурой заменяются единственной реализацией. Что сложного в том, чтобы взять адрес такой функции и записать его в таблицу? Не понимаю.
W>В варианте без шаблонов просто создаётся vtable со всеми возможными сигнатурами уже известных виртуальных функций, а наследники её используют. В случае же шаблонов что и сколько класть в такую vtable неизвестно пока не получен список всех наследников и всех используемых специализаций.
Наследник может добавить новый виртуальный метод в таблицу. Это не сильно отличается от того, что новая специализация может добавить ещё одну строчку в таблицу. Какая в этом сложность?
W>наследник может объявится где-нибудь в сторонней dll или so.
Можно не поддерживать dll и so для этих методов. Кстати, наследник поменяет не больше, чем обычная перегрузка виртуального метода. Поменять таблицу может только новая специализация — она добавляет ещё одну функцию в таблицу. Что в этом сложного?
W>Так экспорт шаблонов наоборот убрали из стандарта. А ждать шаблонные виртуальные методы раньше возврата экспорта ну явно не стоит.
Хмм. А причём тут экспорт шаблонов?
И ещё давай уточним всё же насчёт границ применимости. У тебя какие ограничения предполагаются? Минимальные, когда вся программа умещается в одном cpp файле или более расслабленные? Просто когда есть только один cpp-файл, или хотя бы все шаблонные виртуальные классы со всеми зависимостями заперты в одном cpp-файле, и есть полный список всех допустимых наследников и всех требуемых сигнатур (список хоть ручной, хоть автогенерированный — не важно), то это одна ситуация. А если есть хотя бы два объектных файла с зависимостями между ними, то другая и при этом принципиально более сложная.
BFE>Всё это похоже на отговорки, потому, что: BFE>1. приведённый код можно разбросать по многим файлам и он будет работать (и компилироваться, и линковаться).
Конечно. Но смысл раздельной компиляции и наследования не в том, что можно один cpp файл разбить на два, а в том, что можно отдельно откомпилировать код предка, которому не нужно знать о всех-всех способах использования своих наследников.
BFE>При этом внешняя шаблонная функция, заменяющая таблицу вызовов для виртуальных шаблонных методов, может быть создана кодогенератором (это сложно, но никакой интеллектуальной работы требующей участия человека не требуется)
Верно, составить список в целом не проблема.
BFE>2. реализация вызова виртуальных методов не обязана базироваться на единой таблице — стандарт этого не требует.
Да, можешь и хочешь заменить vtable на что-то другое — пожалуйста.
BFE>Нет ничего страшного в том, чтобы под каждый шаблонный виртуальный метод добавить свою таблицу вызовов для всех специализаций этой функции. Да, все возможные сигнатуры не известны в одном модуле, но что мешает написать сбор всех сигнатур для специализаций этой функции из всех модулей?
Хорошо, запустили компилятор один раз и собрали сигнатуры. А что дальше? Запускать компиляцию по второму кругу чтобы добавить код для найденных сигнатур? Потом ещё раз чтобы пересобрать появившееся зависимости? И ещё и ещё раз пока процесс не сойдётся когда-нибудь? И это не просто каждого файла в отдельности (это как раз тривиально и просто, как у тебя в исходном примере кода и написано), а для совокупности всех исходников и всех библиотек. (Для библиотек, кстати говоря, не для всех доступны исходники чтобы перекомпиляцию запустить — их и объектные файлы тоже нужно запретить в мире c++ как и dll/so?).
Ты так себе видишь процесс компиляции? Если нет, то распиши, пожалуйста, свою схему по-подробнее с точки зрения компилятора: вот мы взяли файл a.cpp (с базовым классом) и на выходе получили такой-то код для таких сигнатур + различные вспомогательные таблицы. Потом взяли файл b.cpp (с наследником) и на выходе получили такой-то код для других сигнатур и другие таблицы. Потом запускам linker и собираем программу. Вот и опиши, что должен делать linker и что должно быть в коде и в таблицах чтобы программа собралась. Для классических виртуальных функций такие действия просты и известны. Для экспорта шаблонов известна пара костылей, которая нормально так и не используется. А что будет в твоём случае?
BFE>Сейчас шаблонные функции тоже разбросаны по разным модулям и что? Ведь все шаблонные функции с одинаковой сигнатурой заменяются единственной реализацией. Что сложного в том, чтобы взять адрес такой функции и записать его в таблицу? Не понимаю.
В этом действии "взять и записать" — ничего. Проблема в том, кто и как будет из таблицы этой адрес читать. То есть проблема в сгенерированном коде.
W>>В варианте без шаблонов просто создаётся vtable со всеми возможными сигнатурами уже известных виртуальных функций, а наследники её используют. В случае же шаблонов что и сколько класть в такую vtable неизвестно пока не получен список всех наследников и всех используемых специализаций. BFE>Наследник может добавить новый виртуальный метод в таблицу. Это не сильно отличается от того, что новая специализация может добавить ещё одну строчку в таблицу. Какая в этом сложность?
Есть отличие принципиальное — наследник не модифицирует при этом код предка. Предок вообще не знает, что какой-то метод у него добавился. Для ВШФ же нужно изменить код предка после обнаружения какой-то специализации и научить предка с этим работать.
W>>наследник может объявится где-нибудь в сторонней dll или so. BFE>Можно не поддерживать dll и so для этих методов. Кстати, наследник поменяет не больше, чем обычная перегрузка виртуального метода. Поменять таблицу может только новая специализация — она добавляет ещё одну функцию в таблицу. Что в этом сложного?
Откуда одна dll узнает, что нужно сгенерировать специализацию foo<int> в базовом классе, если внутри себя она foo<int> не использует, а из другой dll этот метод будет требоваться по зависимостям из какого наследника?
W>>Так экспорт шаблонов наоборот убрали из стандарта. А ждать шаблонные виртуальные методы раньше возврата экспорта ну явно не стоит. BFE>Хмм. А причём тут экспорт шаблонов?
Экспорт шаблонов — это отличная база для реализации шаблонных виртуальных функций. А если у тебя есть шаблонные виртуальные методы, то ты сразу получаешь экспорт шаблонов. По сути в реализации вся сложность именно в экспорте, а виртуальность поверх него уже относительно просто добавить. Но экспорт не реализуется за приемлемую стоимость.
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Есть статья Страуструпа et al. "Open Multi-Methods for C++". Там есть вот такой пункт:
который предварён вот таким предложением:
Future plans to extend our work include:
EP>
EP>8.1 Virtual Function Templates
Это обнадёживает. Собственно, если пойти дальше и добавить к open-method шаблонность, то можно получить ещё более общий вид перегрузок, включающий в себя виртуальные шаблонные методы как подмножество шаблонных open-method'ов.
Здравствуйте, B0FEE664, Вы писали:
BFE>Кто о чём, а я снова о сабже. BFE>Доколе? BFE>В смысле: сколько ещё ждать? BFE>Когда, наконец, добавят в стандарт?
Дженерики уже есть в C++/CLI. Но там механизм совсем другой. Код параметризованной функции не дублируется, это просто виртуальные вызовы плюс более тщательная проверка типов.
Ну и, естественно, унифицированный лэяут для объектов обобщённого типа. В дотнете с этим просто — любой объект хранится по указателю.
Вообще, если бы дженерики появились в неуправляемом С++, это было бы очень здорово.
Здравствуйте, watchmaker, Вы писали:
W>Ты так себе видишь процесс компиляции? Если нет, то распиши, пожалуйста, свою схему по-подробнее с точки зрения компилятора: вот мы взяли файл a.cpp (с базовым классом) и на выходе получили такой-то код для таких сигнатур + различные вспомогательные таблицы. Потом взяли файл b.cpp (с наследником) и на выходе получили такой-то код для других сигнатур и другие таблицы. Потом запускам linker и собираем программу. Вот и опиши, что должен делать linker и что должно быть в коде и в таблицах чтобы программа собралась. Для классических виртуальных функций такие действия просты и известны. Для экспорта шаблонов известна пара костылей, которая нормально так и не используется. А что будет в твоём случае?
Вот так я вижу один из возможных вариантов:
// file: A.hppclass A
{
public:
template <class T>
virtual void virt_fun(T t)
{
std::cout << "A::" << t << std::endl;
}
};
// file: B.hpp#include"A.hpp"class B : public A
{
public:
template <class T>
virtual void virt_fun(T t)
{
std::cout << "B::" << t << std::endl;
}
};
// file: C.cpp#include"A.hpp"void fun_str(A* p)
{
p->virt_fun("fun1");
}
// компиляция этого кода инстанцирует метод A::virt_fun<const char*>
// file: D.cpp#include"A.hpp"void fun_int(A* p)
{
p->virt_fun(32);
}
// компиляция этого кода инстанцирует метод A::virt_fun<int>
// file: E.cpp#include"B.hpp"void fun_float()
{
B b;
b.virt_fun(1.1f);
}
// компиляция этого кода инстанцирует метод B::virt_fun<float>, но не метод A::virt_fun<float>
// file: M.cpp#include"B.hpp"extern void fun_str(A* p);
extern void fun_int(A* p);
extern void fun_float();
int main(int argc, char* argv[])
{
Test oTest;
A* arr[] = {new A, new B};
for(A* p : arr)
{
fun_str(p);
fun_int(p);
}
fun_float();
}
// Компиляция этого кода не инстанцирует ни одного метода virt_fun.
// Таким образом во всех скомпилированных файлах есть только одна реализация для B::virt_fun и это B::virt_fun<float> в E.obj,
// однако,
// известно, что для каждого инстанцированного метода A::virt_fun должн быть инстанцирован соответствующий B::virt_fun метод.
// Информация о том, какие A::virt_fun методы инстанцированы лежат в объектниках C.obj, D.obj и E.obj, но эта информация не доступна
// здесь, в M.obj.
// На стадии линковки, линковщик создаёт таблицу виртуальных функций virt_fun для класса A, для этого он ищет во всех объектниках
// все инстанцирования методов A::virt_fun и находит:
// A::virt_fun<const char*> в C.obj
// A::virt_fun<int> в D.obj
// таким образом, таблица виртуальных функций для класса A готова.
// Далее, исходя из информации о структуре класса B линковщик знает, что для каждой инстанцированой функции A::virt_fun должна
// существовать соответствующая функция B::virt_fun. Линковщик ищет реализацию B::virt_fun<const char*> и B::virt_fun<int>, но не
// находит, поэтому сообщает программисту, что реализации этих функций не найдены.
// Видя такое дело, программист создаёт ещё один файл, в котором пишет код заставляющий компилятор создать указанные реализации,
// которых не хватает. Что это за код - сейчас стандартом не определено, но можно предложить, например, такой синтаксис:
// file: B_impl.cpp#include"B.hpp"//virtualvoid B::virt_fun<const char*>(const char*) = default;
//virtualvoid B::virt_fun<int>(int) = default;
// компиляция этого файла инстанцирует метод B::virt_fun<const char*> и метод B::virt_fun<int>
// После компиляции программист перезапускает линковку добавив B_impl.obj в параметры.
// На стадии поиска реализаций B::virt_fun линковщик находит:
// B::virt_fun<const char*> в B_impl.obj
// B::virt_fun<int> в B_impl.obj
// B::virt_fun<float> в E.obj
//
// Таким образом у линковщика есть вся необходимая информация для построения таблиц виртуальных функций и реализация этих функций:
// Для класса A:
// A::virt_fun<const char*> в C.obj
// A::virt_fun<int> в D.obj
// Для класса B:
// B::virt_fun<const char*> в B_impl.obj
// B::virt_fun<int> в B_impl.obj
// B::virt_fun<float> в E.obj
Вроде бы ничего экстраординарного. Я что-то упускаю? (ну, кроме dll и so, конечно)
Здравствуйте, Кодт, Вы писали:
К>Дженерики уже есть в C++/CLI. Но там механизм совсем другой. Код параметризованной функции не дублируется, это просто виртуальные вызовы плюс более тщательная проверка типов. К>Ну и, естественно, унифицированный лэяут для объектов обобщённого типа. В дотнете с этим просто — любой объект хранится по указателю.
К>Вообще, если бы дженерики появились в неуправляемом С++, это было бы очень здорово.
Здравствуйте, Mr.Delphist, Вы писали:
К>>Вообще, если бы дженерики появились в неуправляемом С++, это было бы очень здорово. MD>Т.е. должно получиться что-то типа такого? MD>
MD>template <generic T>
MD>class C
MD>{
MD>public:
MD> T c;
MD>};
MD>
Скорее,
generic<class T>
class C
{
T* c; // обязательное требование к параметризованным классам - независимость лэяута от параметраpublic:
virtual void set(T* x) { c = x; }
virtual T* get() const { return c; }
generic<class U> where
U : DefaultConstructible<U>, // это значит, что в функцию init будет неявно передан словарь внешних операций над U, содержащий new
U : T // это просто ограничение на тип - подсказка компиляторуvoid init();
};
////// в таком случае реализацию можно будет вытащить прочь
generic<class T> generic<class U> void C<T>::init<U>() { set(new U); }
Здравствуйте, B0FEE664, Вы писали: BFE>Вроде бы ничего экстраординарного. Я что-то упускаю? (ну, кроме dll и so, конечно)
По-моему, такая реализация лишает всю затею смысла:
1. Представьте, что где-то есть шаблон-наследник класса А. Теперь программист начинает ручками дописывать все возможные инстанции для всех виртуальных шаблонов методов?
2. Представьте, что программист знает, что шаблон функции virt_fun для класса B с параметром std::string никогда не потребуется на практике, он используется только для А. Он все равно должен будет сообщить об этом компилятору чем-то вроде:
чтобы, если программа где-то изменится, и этот вызов все-таки произойдет, получить pure virtual function call? Неплохое поле для неожиданных ошибок.
3. Ваша идея неплохо решается на обычном С++ с ручной регистрацией обработчиков — под двум type_id — this и T. Чуть более многословно, но это можно все элегантно порулить макросами.
4. То, что вы предлагаете, по сути, не шаблон виртуальной функции, а автоматизация перегрузки виртуальных функций (которая, и без автоматизации в С++ работает плохо) с перекладыванием части работы на линковщик (что тоже явно С++ придется не по вкусу).
Здравствуйте, Went, Вы писали:
BFE>>Вроде бы ничего экстраординарного. Я что-то упускаю? (ну, кроме dll и so, конечно) W>По-моему, такая реализация лишает всю затею смысла: W>1. Представьте, что где-то есть шаблон-наследник класса А. Теперь программист начинает ручками дописывать все возможные инстанции для всех виртуальных шаблонов методов?
Нет, вы не поняли. Запись:
void B::virt_fun<int>(int) = default;
должна заставлять компилятор сгенерить по уже известному шаблону тело данной функции.
W>2. Представьте, что программист знает, что шаблон функции virt_fun для класса B с параметром std::string никогда не потребуется на практике, он используется только для А. Он все равно должен будет сообщить об этом компилятору чем-то вроде: W>
W>чтобы, если программа где-то изменится, и этот вызов все-таки произойдет, получить pure virtual function call? Неплохое поле для неожиданных ошибок.
Я такого не предлагаю.
W>3. Ваша идея неплохо решается на обычном С++ с ручной регистрацией обработчиков — под двум type_id — this и T. Чуть более многословно, но это можно все элегантно порулить макросами.
Всё можно написать руками, но не удобно — легко допустить ошибку. Собственно, только по этому и нужно то, что я предлагаю.
Я не использую макросы и вам не советую.
W>4. То, что вы предлагаете, по сути, не шаблон виртуальной функции, а автоматизация перегрузки виртуальных функций (которая, и без автоматизации в С++ работает плохо) с перекладыванием части работы на линковщик (что тоже явно С++ придется не по вкусу).
А что же тогда шаблон виртуальной функции?
Здравствуйте, B0FEE664, Вы писали:
BFE>Нет, вы не поняли. Запись: BFE>
BFE>void B::virt_fun<int>(int) = default;
BFE>
BFE>должна заставлять компилятор сгенерить по уже известному шаблону тело данной функции.
Да я понимаю. Представьте, что есть template<typename T> class C : public B {}; И где-то встречается десяток упоминаний C<int>, C<float>, C<some_traits<0>::type> ... C<some_traits<99>::type>, и т.п. И вот наш программист пишет:
void С<int>::virt_fun<int>(int) = default;
void С<int>::virt_fun<float>(float) = default;
void С<float>::virt_fun<int>(int) = default;
void С<float>::virt_fun<float>(float) = default;
void С<some_traits<0>::type>::virt_fun<int>(int) = default;
void С<some_traits<0>::type>::virt_fun<float>(float) = default;
// .... and so on.
То есть получаем ад адового ада в аду. А потом мы меняем сигнатуру функции и ад приходит на землю.
BFE>Я такого не предлагаю.
Но как без этого? Линкер же никогда не догадается, что в функцию
void make_some_virt(A* a)
{
a->virt_fun(std::string("Hello!"));
}
никогда не придет указатель на экземпляр класса B. А значит он потребует от пользователя чтобы эта функция была реализована. А пользователь, со своей стороны скажет: "какого черта?" Реализация B::virt_fun<std::string>(std::string) не имеет смысла и не скомпилируется (например, в шаблоне A::virt_fun, идет какая-то тривиальная работа с аргументом, а в шаблоне B::virt_fun — требующая оператора умножения) и никогда не будет вызвана, но от меня требуется? И что мне делать? И определить нельзя, и не определить нельзя. Придется писать прокси-функцию, которая для правильных типов сделает что надо, а для неправильных — кинет исключение. Так? Подобная проблема встречается и для обычных виртуальных функций у шаблонов классов, но там она решается простой специализацией.
BFE>А что же тогда шаблон виртуальной функции?
Увы, это нонсенс для С++. В чистом виде это "статический полиморфизм времени исполнения", то есть то, что поставит весь С++ с ног на голову. Что действительно было бы полезно, и решало половину проблем, для которых нужны "виртуальные шаблоны", это авто-функции:
class A
{
// Такая функция буде сгенерирована для каждого наследника класса А, если она не определена явно или не помечена как =delete;virtual size_t size() const auto
{
return sizeof(*this);
}
};
Теперь мы можем определять в классе А и его наследниках обычный шаблон, и автоматически размножать его для любой реальной имплементации:
class A
{
// Не-виртуальный шаблон, который по сути будет виртуальнымtemplate<typename T>
void virt_impl(T x)
{
std::cout << x;
}
// Тот же суррогат виртуальности, только один раз записанvirtual void virt_fun(int x) const auto {virt_impl(x);}
virtual void virt_fun(float x) const auto {virt_impl(x);}
virtual void virt_fun(std::string x) const auto {virt_impl(x);}
};
class B : public A
{
// Переопределили реализацию, валидна не для всех типовtemplate<typename T>
void virt_impl(T x)
{
std::cout << x * x;
}
// Удаляем реализацию для строки, будет вызвана базовая.virtual void virt_fun(std::string x) const auto = delete;
}
Вот как-то так. Да, такой подход не дает подсовывать "внезапные" члены в классы на этапе линковки, но при этом ломает на порядок меньше дров и имеет немало других полезных применений.
Здравствуйте, Went, Вы писали:
W>То есть получаем ад адового ада в аду. А потом мы меняем сигнатуру функции и ад приходит на землю.
Сейчас и того хуже, потому что все эти функции придется писать руками с определением тела и т.д. и т.п.
BFE>>Я такого не предлагаю. W>Но как без этого? Линкер же никогда не догадается, что в функцию W>
W>void make_some_virt(A* a)
W>{
a->>virt_fun(std::string("Hello!"));
W>}
W>
W>никогда не придет указатель на экземпляр класса B. W>Так? Подобная проблема встречается и для обычных виртуальных функций у шаблонов классов, но там она решается простой специализацией.
Ничто не мешает определить несколько специализаций и для шаблонной виртуальной функции.
BFE>>А что же тогда шаблон виртуальной функции? W>Увы, это нонсенс для С++. В чистом виде это "статический полиморфизм времени исполнения", то есть то, что поставит весь С++ с ног на голову.
Да ладно! Нет в этом ничего бессмысленного. Это совмещение статического полиморфизма с динамическим.
W>Что действительно было бы полезно, и решало половину проблем, для которых нужны "виртуальные шаблоны", это авто-функции: W>
W>class A
W>{
W> // Такая функция буде сгенерирована для каждого наследника класса А, если она не определена явно или не помечена как =delete;
W> virtual size_t size() const auto
W> {
W> return sizeof(*this);
W> }
W>};
W>
W>Теперь мы можем определять в классе А и его наследниках обычный шаблон, и автоматически размножать его для любой реальной имплементации:
Это другая тема для других задач.
BFE>Вроде бы ничего экстраординарного. Я что-то упускаю? (ну, кроме dll и so, конечно)
Отличное "кроме". Куча софта имеет плагиновую структуру, в которой разные модули, скомпилированные в разные времена и даже разными компиляторами, инстансциирют наследников абстрактных классов. Я уж не говорю про COM и всю бороду которая на нем висит.
Как дополнительная плюшка — типа метавиртуальная функция — всегда пажалста, но существующий механизм виртуальных функций — вещь незаменимая.
Как много веселых ребят, и все делают велосипед...
Здравствуйте, B0FEE664, Вы писали:
BFE>// Линковщик ищет реализацию B::virt_fun<const char*> и B::virt_fun<int>, но не BFE>// находит, поэтому сообщает программисту, что реализации этих функций не найдены. BFE>// Видя такое дело, программист создаёт ещё один файл, в котором пишет код заставляющий компилятор создать указанные реализации, BFE>// которых не хватает.
Ясно, такая реализация понятна. Думал, что тут предполагается какая-то автоматизация, как в том же пресловутом экспорте шаблонов, где именно такое инстнциирование ненайденного могло происходить без правки исходников.
Но, как мне кажется, такая реализация будет особо недружественна к переиспользованию кода: в какой-то вертикальной иерархии классов это использовать можно, но как только от базового класса появится пара наследников в разных частях программы, то между этими частями сразу же возникнет паразитная зависимость.
Так, например, есть у меня есть какой-нибудь logger с шаблонным методом write, а у другого разработчика есть наследник colored_logger, который он использует для своих цветных объектов, то стоит мне написать в своей части проекта logger->write<MyNewType>, так тут же проект перестанет собираться из-за того, что наследник ничего не знает о MyNewType и для него нужно задать реализацию. Вот только проблема в том, что моя и его части проекта могут быть вообще никак не связаны между собой ничем кроме использования общего базового класса. И даже не ясно кто должен добавлять реализацию colored_logger::write<MyNewType> — не я, так как я ничего не знаю об устройстве colored_logger и не другой разработчик, так как он ничего не знает о моём типе MyNewType. И в какую часть кода, в его или в мою (или ввести в проект аналогично отдельный файл B_impl.cpp для такого рода треша)? Но даже если мы договоримся, что чинить должен тот, кто последний сломал, то всё равно сама починка может быть слишком сложной — как уже заметили выше
, совершенно не нет гарантий, что colored_logger::write<MyNewType> = default вообще скомпилируется. Для обычных шаблонов есть SFINAE и возможность задать поведение в этом случае, для мультиметодов есть возможность обобщения, когда при отсутствии специализации происходит откат к более общим правилам, а тут встаёт жёсткое требование — нужно определить поведение для всего декартово произведения классов и шаблонных типов — и никак иначе, даже если многие комбинации будут откровенно абсурдными и в которых ничего разумного кроме throw bad_type сделать не останется.
Кажется, что это слишком сильное требование чтобы использование было по прежнему удобным. Хотя, в каких-то случаях, вроде строго вертикального наследования в изолированной части программы, наверное сойдёт.
Здравствуйте, Went, Вы писали:
W>Да я понимаю. Представьте, что есть template<typename T> class C : public B {}; И где-то встречается десяток упоминаний C<int>, C<float>, C<some_traits<0>::type> ... C<some_traits<99>::type>, и т.п. И вот наш программист пишет: W>
W>void С<int>::virt_fun<int>(int) = default;
W>void С<int>::virt_fun<float>(float) = default;
W>void С<float>::virt_fun<int>(int) = default;
W>void С<float>::virt_fun<float>(float) = default;
W>void С<some_traits<0>::type>::virt_fun<int>(int) = default;
W>void С<some_traits<0>::type>::virt_fun<float>(float) = default;
W>// .... and so on.
W>
W>То есть получаем ад адового ада в аду. А потом мы меняем сигнатуру функции и ад приходит на землю.
Сегодня мне пришло в голову, что для ленивых есть такой трюк. Если не хотите писать этот код руками, то можно заставить компилятор делать это. Для этого достаточно завести один файл, который будет содержать инклюды всех классов иерархии:
// file: virt_fun_impl.hpp#include"C.hpp"#include"D.hpp"#include"E.hpp"// end of file: virt_fun_impl.hpp
Здравствуйте, watchmaker, Вы писали:
BFE>>// Видя такое дело, программист создаёт ещё один файл, в котором пишет код заставляющий компилятор создать указанные реализации, BFE>>// которых не хватает.
W>Ясно, такая реализация понятна. Думал, что тут предполагается какая-то автоматизация, как в том же пресловутом экспорте шаблонов, где именно такое инстнциирование ненайденного могло происходить без правки исходников.
Автоматизацию такого рода можно переложить на среду разработки, а вот компилировать код из шаблонов на этапе линковки — сомнительная идея.
W>Но, как мне кажется, такая реализация будет особо недружественна к переиспользованию кода: в какой-то вертикальной иерархии классов это использовать можно, но как только от базового класса появится пара наследников в разных частях программы, то между этими частями сразу же возникнет паразитная зависимость. W>Так, например, есть у меня есть какой-нибудь logger с шаблонным методом write, а у другого разработчика есть наследник colored_logger, который он использует для своих цветных объектов, то стоит мне написать в своей части проекта logger->write<MyNewType>, так тут же проект перестанет собираться из-за того, что наследник ничего не знает о MyNewType и для него нужно задать реализацию. Вот только проблема в том, что моя и его части проекта могут быть вообще никак не связаны между собой ничем кроме использования общего базового класса.
Это самый интересный аргумент против виртуальных шаблонных функций, что я встречал. С одной стороны — да, такая проблема весьма неприятна, а вот с другой в ней нет ничего удивительного. Виртуальная шаблонная функция включает в себя как недостатки виртуальной функции, так и недостатки шаблонной функции. А именно:
— если вы поменяли тип параметра виртуальной функции, то вам придётся править всю иерархию наследников;
— указанный параметр шаблонной функции должен соблюдать требования накладываемые шаблонной функцией на тип параметра.
Так что если проект не собирается, то кто-то сделал ошибку.
W>И даже не ясно кто должен добавлять реализацию colored_logger::write<MyNewType> — не я, так как я ничего не знаю об устройстве colored_logger и не другой разработчик, так как он ничего не знает о моём типе MyNewType. И в какую часть кода, в его или в мою (или ввести в проект аналогично отдельный файл B_impl.cpp для такого рода треша)? Но даже если мы договоримся, что чинить должен тот, кто последний сломал, то всё равно сама починка может быть слишком сложной — как уже заметили выше
W> совершенно не нет гарантий, что colored_logger::write<MyNewType> = default вообще скомпилируется. W>Для обычных шаблонов есть SFINAE и возможность задать поведение в этом случае, для мультиметодов есть возможность обобщения, когда при отсутствии специализации происходит откат к более общим правилам, а тут встаёт жёсткое требование — нужно определить поведение для всего декартово произведения классов и шаблонных типов — и никак иначе, даже если многие комбинации будут откровенно абсурдными и в которых ничего разумного кроме throw bad_type сделать не останется.
А почему, собственно, SFINAE не будет работать? Что этому помешает?
Здравствуйте, B0FEE664, Вы писали:
BFE>С одной стороны — да, такая проблема весьма неприятна, а вот с другой в ней нет ничего удивительного. Виртуальная шаблонная функция включает в себя как недостатки виртуальной функции, так и недостатки шаблонной функции.
Но можно ведь часть недостатков скомпенсировать. Некоторые проблемы, действительно, будут присутствовать у всех виртуальных шаблонных функций, а некоторые — только в этой реализации.
Например, возможность не делать вручную квадратичное число определений, или возможность задать действие в случае невозможности совместить типы, а не просто выдавать ошибку undefined reference.
BFE>- если вы поменяли тип параметра виртуальной функции, то вам придётся править всю иерархию наследников;
Да даже с обычными функциями такая же ситуация — поменял сигнатуру и поменял все места использования. Это не исключительно виртуальных функций недостаток.
BFE>- указанный параметр шаблонной функции должен соблюдать требования накладываемые шаблонной функцией на тип параметра. BFE>Так что если проект не собирается, то кто-то сделал ошибку.
Так кто из двух программистов из примера выше сделал ошибку? Первый, написавший logger->write<MyNewType> её не сделал, так как MyNewType удовлетворяет всем ограничениям базового класса. Второй вообще код не менял. Код сам сломался. Или, быть может, их ошибкой была сама идея использовать виртуальные шаблонные функции!? Я уже почти уверен, что именно это
BFE>А почему, собственно, SFINAE не будет работать? Что этому помешает?
Тут, как мне кажется, сломана заключительная часть — что делать если ни одна подстановка не сработала. Если у меня есть шаблонная функция Foo<>, а я вызываю Foo<Bar> с несовместимым типом Bar, то я могу поймать этот момент через ellipsis, например, и задать поведение в этом случае.
В вышеприведённой же реализации linker сразу смотрит на наличие единственного символа и всё заканчивается на обнаружении его отсутствия. Нет простой возможности задать поведение сразу для группы плохих комбинаций. В тех же мультиметодах такая возможность есть — если не определена частная реализация, то откатываемся к более общей. А linker'у это будет намного сложнее объяснить. Короче говоря, не хватает аналога записи void B::virt_fun<int>(int) = default;, но которая будет задавать действие когда default подстановка не удалась. Возможно какой-то синтаксис вроде non-override, позволяющий взять в таком случае реализацию из базового класса (хотя тут можно что-то случайно сломать и не сразу заметить; да и ругаться в runtime может скорее понадобится, а не наследовать), или ввести opaque наследование для классов, чтобы наличие метода Foo<Bar> в одной части программы не вызывало необходимости наличия такой же специализации у наследника в другой части программы (если программист честно пообещает, что это части никак не связаны).
Здравствуйте, watchmaker, Вы писали:
W>Например, возможность не делать вручную квадратичное число определений, или возможность задать действие в случае невозможности совместить типы, а не просто выдавать ошибку undefined reference.
В том варианте, что я предлагаю присутствует возможность не делать квадратичное число определений вручную. Но если кто-то хочет сделать определения вручную, то он может этим заняться для уменьшения времени компиляции.
BFE>>- указанный параметр шаблонной функции должен соблюдать требования накладываемые шаблонной функцией на тип параметра. BFE>>Так что если проект не собирается, то кто-то сделал ошибку. W>Так кто из двух программистов из примера выше сделал ошибку? Первый, написавший logger->write<MyNewType> её не сделал, так как MyNewType удовлетворяет всем ограничениям базового класса. Второй вообще код не менял. Код сам сломался. Или, быть может, их ошибкой была сама идея использовать виртуальные шаблонные функции!? Я уже почти уверен, что именно это
Почему тот, кто написал logger->write<MyNewType> не может добавить в свой файл строчку #include, которая решить его проблему с undefined reference? Очевидно, что написавший logger->write<MyNewType> забыл добавить определение функции, которое лежит в одном из хедеров. Всё равно, что написать:
Что удивительного в том, что линкер не соберёт такой проект?
BFE>>А почему, собственно, SFINAE не будет работать? Что этому помешает? W>Тут, как мне кажется, сломана заключительная часть — что делать если ни одна подстановка не сработала. Если у меня есть шаблонная функция Foo<>, а я вызываю Foo<Bar> с несовместимым типом Bar, то я могу поймать этот момент через ellipsis, например, и задать поведение в этом случае. W>В вышеприведённой же реализации linker сразу смотрит на наличие единственного символа и всё заканчивается на обнаружении его отсутствия.
Если есть декларация функции с ellipsis, но нет определения, то linker не сможет слинковать проект.
W>Нет простой возможности задать поведение сразу для группы плохих комбинаций. В тех же мультиметодах такая возможность есть — если не определена частная реализация, то откатываемся к более общей. А linker'у это будет намного сложнее объяснить.
Ну причём тут мультиметоды? Кто-то предлагает реализацию шаблонных мультиметодов?
W>Короче говоря, не хватает аналога записи void B::virt_fun<int>(int) = default;, но которая будет задавать действие когда default подстановка не удалась. Возможно какой-то синтаксис вроде non-override, позволяющий взять в таком случае реализацию из базового класса (хотя тут можно что-то случайно сломать и не сразу заметить; да и ругаться в runtime может скорее понадобится, а не наследовать), или ввести opaque наследование для классов, чтобы наличие метода Foo<Bar> в одной части программы не вызывало необходимости наличия такой же специализации у наследника в другой части программы (если программист честно пообещает, что это части никак не связаны).
Да зачем же такое нужно? Если чего-то не удалось, то это ошибка в программе. А то, что вы предлагаете — прямой путь к нарушению ODR: может сложится ситуация, когда одинаковый код вызывает разные функции в зависимости от того, какие хедеры подключены.
Не забывайте, что речь идёт о шаблонных функциях, а шаблонные функции требуют включения в модуль всех определений шаблонной функции. Что же удивительного в том, что при отсутствии части определений проект не соберётся?
Здравствуйте, B0FEE664, Вы писали:
BFE>Сейчас и того хуже, потому что все эти функции придется писать руками с определением тела и т.д. и т.п.
Сейчас не хуже, сейчас нет лишних усложнений системы ради сомнительного функционала. Если уже вводить кросс-модульную кодогенерацию, то подходить к этому нужно основательно, спрятать под это внешние определения шаблонов и много других потенциальных ништяков. Сами по себе виртуальные шаблоны этого не стоят, я считаю.
BFE>Ничто не мешает определить несколько специализаций и для шаблонной виртуальной функции.
Разница в том, что здесь придется определять специализации, которые будут заранее не нужны, для функций, которые никогда не будут вызваны. Короче, это обсуждалось достаточно.
W>>Увы, это нонсенс для С++. В чистом виде это "статический полиморфизм времени исполнения", то есть то, что поставит весь С++ с ног на голову. BFE>Это совмещение статического полиморфизма с динамическим.
Что есть нонсенс для современного С++. А чтобы он нонсенсом быть перестал, нужно перестраивать всю идеологию кодогенерации.
BFE>Это другая тема для других задач.
Разве? А мне показалось, что эта тема вполне разумно решила именно нашу задачу.
Здравствуйте, Went, Вы писали: BFE>>Сейчас и того хуже, потому что все эти функции придется писать руками с определением тела и т.д. и т.п. W>Сейчас не хуже, сейчас нет лишних усложнений системы ради сомнительного функционала.
Про виртуальные функции так же говорили. W>Если уже вводить кросс-модульную кодогенерацию, то подходить к этому нужно основательно, спрятать под это внешние определения шаблонов и много других потенциальных ништяков.
Я как раз предлагаю не делать кросс-модульную кодогенерацию, так как эта слишком сложно и не все с эти справятся. W>Сами по себе виртуальные шаблоны этого не стоят, я считаю.
Мне очень нужны виртуальные шаблонные методы. Уже который год. BFE>>Ничто не мешает определить несколько специализаций и для шаблонной виртуальной функции. W>Разница в том, что здесь придется определять специализации, которые будут заранее не нужны, для функций, которые никогда не будут вызваны. Короче, это обсуждалось достаточно.
Можно посмотреть, где это обсуждалось? W>>>Увы, это нонсенс для С++. В чистом виде это "статический полиморфизм времени исполнения", то есть то, что поставит весь С++ с ног на голову. BFE>>Это совмещение статического полиморфизма с динамическим. W>Что есть нонсенс для современного С++. А чтобы он нонсенсом быть перестал, нужно перестраивать всю идеологию кодогенерации.
Зачем? Почему нельзя сделать как я предлагаю? BFE>>Это другая тема для других задач. W>Разве? А мне показалось, что эта тема вполне разумно решила именно нашу задачу.
А как вы видите себе использование шаблонных виртуальных методов?
Я, например, вижу применение позволяющие писать хранение разнотипных объектов в одном контейнере и их обработку с помощью единого метода.
Конкретно это может выглядеть так:
— создаём объект очередь;
— добавляем в очередь данные произвольного типа;
— создаём диспетчер сообщений, содержащий обработчики для каждого из типов;
— вызываем у очереди функцию pop передав ей в качестве параметра диспетчер. Эта функция вызывает обработчик находя соответствие между типом данных и обработчиком.
Насколько я знаю, сейчас нет нормальных способов написать такую очередь для произвольного набора типов. Сейчас это делают либо с помощью RTTI, либо с помощью стирателя типа, либо с помощью жутких приведений типов, либо для ограниченного набора типов. Если знаете как написать такую очередь красиво, скажите мне, пожалуйста.
А вот если бы у нас были виртуальные шаблонные методы это выглядело бы довольно красиво:
class HolderBase
{
public:
template <class Fn>
virtual void Dispatch(Fn f)
{
// it should call Holder<T>::Dispatch(f);
}
};
template <class T>
class Holder : public HolderBase
{
public:
Holder(const T& t)
: m_t(t)
{
}
template <class Fn>
virtual void Dispatch(Fn f)
{
f(t);
}
private:
T m_t;
};
class Queue
{
public:
template <class T>
void push(T t)
{
m_queue.push(new Holder<T>(t));// TODO smart ptr?
};
template <class TDispatcher>
void pop(TDispatcher oDispatcher)
{
assert(!m_queue.empty());
HolderBase* p = m_queue.front(); // TODO RAII
m_queue.pop();
p->Dispatch(oDispatcher);
delete p;
};
bool empty() const
{
return m_queue.empty();
}
private:
std::queue<HolderBase*> m_queue;
};
Как тут помогут авто-функции я не вижу.
весь код
#include <assert.h>
#include <iostream>
#include <queue>
#include <string>
class HolderBase
{
public:
template <class Fn>
void Dispatch(Fn f)
{
// it should call Holder<T>::Dispatch(f);
}
};
template <class T>
class Holder : public HolderBase
{
public:
Holder(const T& t)
: m_t(t)
{
}
template <class Fn>
void Dispatch(Fn f)
{
f(t);
}
private:
T m_t;
};
class Queue
{
public:
template <class T>
void push(T t)
{
m_queue.push(new Holder<T>(t));// TODO smart ptr?
};
template <class TDispatcher>
void pop(TDispatcher oDispatcher)
{
assert(!m_queue.empty());
HolderBase* p = m_queue.front(); // TODO RAII
m_queue.pop();
p->Dispatch(oDispatcher);
delete p;
};
bool empty() const
{
return m_queue.empty();
}
private:
std::queue<HolderBase*> m_queue;
};
//-------------------------------------------------------------------------------------------DispatcherSettemplate<class ...TDispatchers>
class DispatcherSet;
template<class TDispatcher, class ...TDispatchers>
class DispatcherSet<TDispatcher, TDispatchers...> : public TDispatcher, public DispatcherSet<TDispatchers...>
{
public:
DispatcherSet(TDispatcher oDispatcher, TDispatchers... oDispatchers)
: TDispatcher (oDispatcher),
DispatcherSet<TDispatchers...>(oDispatchers...)
{
}
using TDispatcher::operator();
using DispatcherSet<TDispatchers...>::operator();
};
template<class TDispatcher>
class DispatcherSet<TDispatcher> : public TDispatcher
{
public:
DispatcherSet(TDispatcher oDispatcher)
: TDispatcher(oDispatcher)
{
}
using TDispatcher::operator();
};
template<class ...TDispatchers>
DispatcherSet<TDispatchers...> CreateDispatchers(TDispatchers... oDispatchers)
{
return DispatcherSet<TDispatchers...>(oDispatchers...);
}
//-------------------------------------------------------------------------------------------void IntLog(int n)
{
std::cout << "IntLog: n = " << n << std::endl;
}
void StrLog(const std::string& str)
{
std::cout << "StrLog: str = " << str << std::endl;
}
//-------------------------------------------------------------------------------------------int main(int argc, char* argv[])
{
Queue q;
int n = 0;
std::string str("asdf");
q.push(n);
q.push(str);
auto oDispatcher = CreateDispatchers(
[](int n ){ IntLog(n); },
[](const std::string& str){ StrLog(str); },
[](float f ){ std::cout << "float = " << f << std::endl; }
);
// test
//oDispatcher(n);
//oDispatcher(n);
//oDispatcher(str);while(!q.empty())
q.pop(oDispatcher);
return 0;
}
Здравствуйте, B0FEE664, Вы писали: BFE>Насколько я знаю, сейчас нет нормальных способов написать такую очередь для произвольного набора типов. Сейчас это делают либо с помощью RTTI, либо с помощью стирателя типа, либо с помощью жутких приведений типов, либо для ограниченного набора типов. Если знаете как написать такую очередь красиво, скажите мне, пожалуйста.
Для такого общего случая автофункции не помогут. Поэтому я и писал про половину Но проблема в том, что и ваше решение содержит слишком много костылей. Поэтому я и говорю, что еще можно поспорить — какое решение лучше: RTTI, стиратели, жуткие приведения или "виртуальные шаблоны". Ведь недостаточно будет "докомпилировать все недостающие виртуальные перекрытия" для всех встреченных (и не встреченных, но подсказанных программистом) вариантов. Придется отложить и компиляцию всех функций, которые вызывают эту виртуальную функцию (нужно же будет знать, из какого многообразия перекрытий мы выбираем), и даже тех функций, которые вызывают любые другие виртуальные функции этого класса (ведь размер виртуальной таблицы будет не определен до момента полной компиляции, а значит смещения в этой таблице будут скакать). Мало того, так как любой вызов виртуальной функции будет по факту являться поводом для инстанции нового экземпляра, мы легко получим случаи, когда вместо определения диспетчера для int нам потребуется определение оного для const int, для unsigned int, сonst unsigned int, const int&, short, char, long и прочего зоопарка. Ведь как компилятор догадается, что нужно использовать готовую реализацию, а не затребовать новую? Вы предлагаете делать связки определений, которые потом "включать все вместе в нужных местах". Но это противоречит идее произвольного набора типов. Набор уже не произволен, а ограничен тем, что включено. А если он ограничен, то можно использовать обычного Visitor-а и не ломать компиляторы
Здравствуйте, Went, Вы писали:
W> Но проблема в том, что и ваше решение содержит слишком много костылей. Поэтому я и говорю, что еще можно поспорить — какое решение лучше: RTTI, стиратели, жуткие приведения или "виртуальные шаблоны". Ведь недостаточно будет "докомпилировать все недостающие виртуальные перекрытия" для всех встреченных (и не встреченных, но подсказанных программистом) вариантов.
Объясните, почему недостаточно? В каком случае недостаточно?
W> Придется отложить и компиляцию всех функций, которые вызывают эту виртуальную функцию (нужно же будет знать, из какого многообразия перекрытий мы выбираем),
Вообще-то все разнообразие задано при декларации класса и многообразие перекрытий его поменять не может.
W>и даже тех функций, которые вызывают любые другие виртуальные функции этого класса (ведь размер виртуальной таблицы будет не определен до момента полной компиляции, а значит смещения в этой таблице будут скакать).
Это не аргумент. Это отговорка.
W>Мало того, так как любой вызов виртуальной функции будет по факту являться поводом для инстанции нового экземпляра, мы легко получим случаи, когда вместо определения диспетчера для int нам потребуется определение оного для const int, для unsigned int, сonst unsigned int, const int&, short, char, long и прочего зоопарка. Ведь как компилятор догадается, что нужно использовать готовую реализацию, а не затребовать новую?
Насколько я понимаю, компиляторы уже сейчас с этим справляется объединяя код для void TFun<int>(int) и void TFun<const int>(const int) функции template <class T> void TFun(T t), так что я не вижу здесь проблемы.
Даже если и потребуется однострочное определения для каждого типа, то это всё легче, чем писать полные определения для каждого из типов, как это делается сейчас.
W>Вы предлагаете делать связки определений, которые потом "включать все вместе в нужных местах". Но это противоречит идее произвольного набора типов. Набор уже не произволен, а ограничен тем, что включено. А если он ограничен, то можно использовать обычного Visitor-а и не ломать компиляторы
Если вы используете обычный Visitor, то вам приходится руками писать весь тот "зоопарк" функций в каждой отдельной реализации этого паттерна. В каждой реализации, Went! Для каждого отдельного набора типов! Полноценную реализацию каждой функции для каждого отдельного типа при каждом использовании паттерна в каждом отдельном проекте! А с виртуальными шаблонными весь паттерн укладывается в один-два экрана кода без потери общности и скорости вызовов. Но всех смущают однострочные определения под конкретные типы. Пойду выпью воды...
Здравствуйте, B0FEE664, Вы писали:
BFE>Объясните, почему недостаточно? В каком случае недостаточно?
Объясняю как раз ниже:
BFE>Вообще-то все разнообразие задано при декларации класса и многообразие перекрытий его поменять не может.
Ну смотрите. У нас есть класс:
Какое смещение должен взять компилятор для определения адреса X::omg? Пока мы не узнаем все многообразие вызовов foo и bar, встречающееся в программе, мы этого не узнаем. А к чему это приведет? К тому, что любой код, использующий данный вызов будет вынужден перекомпилироваться при каждой перелинковке программы.
BFE>Если вы используете обычный Visitor, то вам приходится руками писать весь тот "зоопарк" функций в каждой отдельной реализации этого паттерна. В каждой реализации, Went! Для каждого отдельного набора типов! Полноценную реализацию каждой функции для каждого отдельного типа при каждом использовании паттерна в каждом отдельном проекте! А с виртуальными шаблонными весь паттерн укладывается в один-два экрана кода без потери общности и скорости вызовов. Но всех смущают однострочные определения под конкретные типы. Пойду выпью воды...
На вариадиках можно замутить довольно элегантно.
Здравствуйте, Went, Вы писали:
W>Здравствуйте, B0FEE664, Вы писали:
BFE>>Объясните, почему недостаточно? В каком случае недостаточно? W>Объясняю как раз ниже:
W>
W>void f(X* px)
W>{
x->omg();
W>}
W>
W>Какое смещение должен взять компилятор для определения адреса X::omg? Пока мы не узнаем все многообразие вызовов foo и bar, встречающееся в программе, мы этого не узнаем.
Вообще говоря, нет такой уж необходимости решать этот вопрос на этапе компиляции. У тебя задан вопрос «какое брать смещение?», а я могу задать другой вопрос — «смещение относительно чего?».
Если рассмотреть самую популярную реализация обычных виртуальных функций через vtable, то там смещение берётся от начала соответствующей таблицы. И хотя список записей в этой таблице и известен на этапе компиляции, но её адрес — нет.
Да, этот адрес записан в экземпляре объекта, но это уже runtime, а сам он вычисляется лишь на этапе линковки. Но это совершенно не мешает компилятору создавать код.
Просто компилятор может генерировать инструкции, которые загружают константы, которые вычисляются лишь линкером. Что он обычно и делает: в коде конструктора оставляются несколько инструкций, которые записывают в vptr экземпляра нужный указатель, но при этом после компиляции вместо этого указателя там содержится заглушка, а линкеру оставляются инструкции подставить на это место актуальное значение когда-то потом.
Так что возвращаясь к вопросу о смещении X::omg — оно может быть вычислено линкером. А компилятор сможет загрузить это смещение, например, из константы в памяти, куда значение положит линкер (либо линкер может даже напрямую внутрь самой инструкции нужное смещение записать).
Да, такая реализация (с неизвестной структурой таблиц на этапе компиляции) обладает многими недостатками, но нет принципиальной ограничений, из-за которых было бы нельзя усложнить линкер и переложить на него задачу сборки таблиц.
Здравствуйте, watchmaker, Вы писали:
W>Экспорт шаблонов — это отличная база для реализации шаблонных виртуальных функций. А если у тебя есть шаблонные виртуальные методы, то ты сразу получаешь экспорт шаблонов. По сути в реализации вся сложность именно в экспорте, а виртуальность поверх него уже относительно просто добавить. Но экспорт не реализуется за приемлемую стоимость.
Даже есть бы был экспорт, всё равно vtable пришлось бы как-то динамически хачить...
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
К>Вообще, если бы дженерики появились в неуправляемом С++, это было бы очень здорово.
А что мешает?
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Здравствуйте, Erop, Вы писали:
К>>Вообще, если бы дженерики появились в неуправляемом С++, это было бы очень здорово. E>А что мешает?
1) продумать неинтрузивные словари функций (в дополнение к интрузивным — таблицам виртуальных методов) и способы их передачи.
2) придумать удобный боксинг данных для унификации лэяута (не ограничиваться одним лишь shared_ptr и всякими ptr_контейнерами); удобный — в идеале, это неявный.
3) взять спецификацию C++CLI и перетащить синтаксис в стандарт, а реализацию парсера и тайпчекера в компиляторы.
Это всё — время, деньги, люди...
Микрософту это, вроде, не особо нужно, C++CLI бедный родственник. А остальные будут делать с нуля.