шаблонный виртуальный метод
От: B0FEE664  
Дата: 29.06.15 16:07
Оценка: :)
Кто о чём, а я снова о сабже.
Доколе?
В смысле: сколько ещё ждать?
Когда, наконец, добавят в стандарт?

Скептикам и тем, кому нужны 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;
}
И каждый день — без права на ошибку...
Re: шаблонный виртуальный метод
От: watchmaker  
Дата: 29.06.15 16:26
Оценка: +4
Здравствуйте, B0FEE664, Вы писали:


BFE>Нет ничего сложного в том, чтобы компилятор сгенерил CallFunImpl для каждого виртуального шаблонного метода, но писателем компиляторов что-то мешает. Кто-нибудь знает, что именно им мешает?

Так раздельная компиляция же. Пока вся программа умещается в одном файле, действительно, нет ничего сложного. Как только появляется пара объектных файлов, то внезапно не получается в реализации базового класса перечислить всех его наследников и все используемые в них шаблонные специализации, о которых базовый класс на момент компиляции ничего не знает. В варианте без шаблонов просто создаётся vtable со всеми возможными сигнатурами уже известных виртуальных функций, а наследники её используют. В случае же шаблонов что и сколько класть в такую vtable неизвестно пока не получен список всех наследников и всех используемых специализаций. И даже техники вроде whole program optimization такой список построить не помогут — ведь наследник может объявится где-нибудь в сторонней dll или so.

BFE>В смысле: сколько ещё ждать?

BFE>Когда, наконец, добавят в стандарт?
Так экспорт шаблонов наоборот убрали из стандарта. А ждать шаблонные виртуальные методы раньше возврата экспорта ну явно не стоит.
Re: шаблонный виртуальный метод
От: Evgeny.Panasyuk Россия  
Дата: 29.06.15 16:58
Оценка: 4 (1)
Есть статья Страуструпа et al. "Open Multi-Methods for C++". Там есть вот такой пункт:

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.

Отредактировано 29.06.2015 16:59 Evgeny.Panasyuk . Предыдущая версия .
Re[2]: шаблонный виртуальный метод
От: B0FEE664  
Дата: 29.06.15 17:42
Оценка: -1
Здравствуйте, watchmaker, Вы писали:

BFE>>Нет ничего сложного в том, чтобы компилятор сгенерил CallFunImpl для каждого виртуального шаблонного метода, но писателем компиляторов что-то мешает. Кто-нибудь знает, что именно им мешает?

W>Так раздельная компиляция же. Пока вся программа умещается в одном файле, действительно, нет ничего сложного. Как только появляется пара объектных файлов, то внезапно не получается в реализации базового класса перечислить всех его наследников и все используемые в них шаблонные специализации, о которых базовый класс на момент компиляции ничего не знает. В варианте без шаблонов просто создаётся vtable со всеми возможными сигнатурами уже известных виртуальных функций, а наследники её используют. В случае же шаблонов что и сколько класть в такую vtable неизвестно пока не получен список всех наследников и всех используемых специализаций. И даже техники вроде whole program optimization такой список построить не помогут — ведь наследник может объявится где-нибудь в сторонней dll или so.

Всё это похоже на отговорки, потому, что:
1. приведённый код можно разбросать по многим файлам и он будет работать (и компилироваться, и линковаться). При этом внешняя шаблонная функция, заменяющая таблицу вызовов для виртуальных шаблонных методов, может быть создана кодогенератором (это сложно, но никакой интеллектуальной работы требующей участия человека не требуется)
2. реализация вызова виртуальных методов не обязана базироваться на единой таблице — стандарт этого не требует. Нет ничего страшного в том, чтобы под каждый шаблонный виртуальный метод добавить свою таблицу вызовов для всех специализаций этой функции. Да, все возможные сигнатуры не известны в одном модуле, но что мешает написать сбор всех сигнатур для специализаций этой функции из всех модулей? Сейчас шаблонные функции тоже разбросаны по разным модулям и что? Ведь все шаблонные функции с одинаковой сигнатурой заменяются единственной реализацией. Что сложного в том, чтобы взять адрес такой функции и записать его в таблицу? Не понимаю.

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

Наследник может добавить новый виртуальный метод в таблицу. Это не сильно отличается от того, что новая специализация может добавить ещё одну строчку в таблицу. Какая в этом сложность?

W>наследник может объявится где-нибудь в сторонней dll или so.

Можно не поддерживать dll и so для этих методов. Кстати, наследник поменяет не больше, чем обычная перегрузка виртуального метода. Поменять таблицу может только новая специализация — она добавляет ещё одну функцию в таблицу. Что в этом сложного?

W>Так экспорт шаблонов наоборот убрали из стандарта. А ждать шаблонные виртуальные методы раньше возврата экспорта ну явно не стоит.

Хмм. А причём тут экспорт шаблонов?
И каждый день — без права на ошибку...
Re[3]: шаблонный виртуальный метод
От: watchmaker  
Дата: 29.06.15 18:48
Оценка:
Здравствуйте, B0FEE664, Вы писали:

И ещё давай уточним всё же насчёт границ применимости. У тебя какие ограничения предполагаются? Минимальные, когда вся программа умещается в одном 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>Хмм. А причём тут экспорт шаблонов?
Экспорт шаблонов — это отличная база для реализации шаблонных виртуальных функций. А если у тебя есть шаблонные виртуальные методы, то ты сразу получаешь экспорт шаблонов. По сути в реализации вся сложность именно в экспорте, а виртуальность поверх него уже относительно просто добавить. Но экспорт не реализуется за приемлемую стоимость.
Отредактировано 29.06.2015 19:10 watchmaker . Предыдущая версия .
Re[2]: шаблонный виртуальный метод
От: B0FEE664  
Дата: 29.06.15 22:27
Оценка:
Здравствуйте, 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'ов.

Только я не понял: какого года статья?
И каждый день — без права на ошибку...
Re[3]: шаблонный виртуальный метод
От: Evgeny.Panasyuk Россия  
Дата: 29.06.15 22:57
Оценка:
Здравствуйте, B0FEE664, Вы писали:

BFE>Только я не понял: какого года статья?


Судя по всему 2007.
Re: шаблонный виртуальный метод
От: Кодт Россия  
Дата: 30.06.15 10:51
Оценка:
Здравствуйте, B0FEE664, Вы писали:

BFE>Кто о чём, а я снова о сабже.

BFE>Доколе?
BFE>В смысле: сколько ещё ждать?
BFE>Когда, наконец, добавят в стандарт?

Дженерики уже есть в C++/CLI. Но там механизм совсем другой. Код параметризованной функции не дублируется, это просто виртуальные вызовы плюс более тщательная проверка типов.
Ну и, естественно, унифицированный лэяут для объектов обобщённого типа. В дотнете с этим просто — любой объект хранится по указателю.

Вообще, если бы дженерики появились в неуправляемом С++, это было бы очень здорово.
Перекуём баги на фичи!
Re[4]: шаблонный виртуальный метод
От: B0FEE664  
Дата: 30.06.15 10:57
Оценка:
Здравствуйте, watchmaker, Вы писали:

W>Ты так себе видишь процесс компиляции? Если нет, то распиши, пожалуйста, свою схему по-подробнее с точки зрения компилятора: вот мы взяли файл a.cpp (с базовым классом) и на выходе получили такой-то код для таких сигнатур + различные вспомогательные таблицы. Потом взяли файл b.cpp (с наследником) и на выходе получили такой-то код для других сигнатур и другие таблицы. Потом запускам linker и собираем программу. Вот и опиши, что должен делать linker и что должно быть в коде и в таблицах чтобы программа собралась. Для классических виртуальных функций такие действия просты и известны. Для экспорта шаблонов известна пара костылей, которая нормально так и не используется. А что будет в твоём случае?


Вот так я вижу один из возможных вариантов:
// file: A.hpp
class 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"

//virtual
void B::virt_fun<const char*>(const char*) = default;

//virtual
void 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, конечно)
И каждый день — без права на ошибку...
Re[2]: шаблонный виртуальный метод
От: Mr.Delphist  
Дата: 30.06.15 12:48
Оценка:
Здравствуйте, Кодт, Вы писали:

К>Дженерики уже есть в C++/CLI. Но там механизм совсем другой. Код параметризованной функции не дублируется, это просто виртуальные вызовы плюс более тщательная проверка типов.

К>Ну и, естественно, унифицированный лэяут для объектов обобщённого типа. В дотнете с этим просто — любой объект хранится по указателю.

К>Вообще, если бы дженерики появились в неуправляемом С++, это было бы очень здорово.


Т.е. должно получиться что-то типа такого?

template <generic T>
class C
{
public:
    T c;
};
Re[3]: шаблонный виртуальный метод
От: Кодт Россия  
Дата: 30.06.15 13:17
Оценка:
Здравствуйте, 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); }


Реализация "на голом С++" выглядит примерно так
class C_gen
{
  void* c;
public:
  virtual void set(void* x) { c = x; }
  virtual void* get() const { return c; }
  virtual void init(DefaultConstructible* traits) { set(traits->call_new()); }
};

template<class T> class C_facade : public C_gen
{
public:
  void set(T* x) { C_gen::set(x); }
  T* get() const { return (T*)C_gen::get(); }
  template<class U> void init() { C_gen::init(DefaultConstructible_impl<U>::instance()); }
};
Перекуём баги на фичи!
Re[5]: шаблонный виртуальный метод
От: Went  
Дата: 30.06.15 14:57
Оценка:
Здравствуйте, B0FEE664, Вы писали:
BFE>Вроде бы ничего экстраординарного. Я что-то упускаю? (ну, кроме dll и so, конечно)
По-моему, такая реализация лишает всю затею смысла:
1. Представьте, что где-то есть шаблон-наследник класса А. Теперь программист начинает ручками дописывать все возможные инстанции для всех виртуальных шаблонов методов?
2. Представьте, что программист знает, что шаблон функции virt_fun для класса B с параметром std::string никогда не потребуется на практике, он используется только для А. Он все равно должен будет сообщить об этом компилятору чем-то вроде:
//virtual
void B::virt_fun<std::string>(std::string) = 0;

чтобы, если программа где-то изменится, и этот вызов все-таки произойдет, получить pure virtual function call? Неплохое поле для неожиданных ошибок.
3. Ваша идея неплохо решается на обычном С++ с ручной регистрацией обработчиков — под двум type_id — this и T. Чуть более многословно, но это можно все элегантно порулить макросами.
4. То, что вы предлагаете, по сути, не шаблон виртуальной функции, а автоматизация перегрузки виртуальных функций (которая, и без автоматизации в С++ работает плохо) с перекладыванием части работы на линковщик (что тоже явно С++ придется не по вкусу).
Re[6]: шаблонный виртуальный метод
От: B0FEE664  
Дата: 30.06.15 15:11
Оценка:
Здравствуйте, Went, Вы писали:

BFE>>Вроде бы ничего экстраординарного. Я что-то упускаю? (ну, кроме dll и so, конечно)

W>По-моему, такая реализация лишает всю затею смысла:
W>1. Представьте, что где-то есть шаблон-наследник класса А. Теперь программист начинает ручками дописывать все возможные инстанции для всех виртуальных шаблонов методов?
Нет, вы не поняли. Запись:
void B::virt_fun<int>(int) = default;

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

W>2. Представьте, что программист знает, что шаблон функции virt_fun для класса B с параметром std::string никогда не потребуется на практике, он используется только для А. Он все равно должен будет сообщить об этом компилятору чем-то вроде:

W>
W>//virtual
W>void B::virt_fun<std::string>(std::string) = 0;
W>

W>чтобы, если программа где-то изменится, и этот вызов все-таки произойдет, получить pure virtual function call? Неплохое поле для неожиданных ошибок.

Я такого не предлагаю.

W>3. Ваша идея неплохо решается на обычном С++ с ручной регистрацией обработчиков — под двум type_id — this и T. Чуть более многословно, но это можно все элегантно порулить макросами.

Всё можно написать руками, но не удобно — легко допустить ошибку. Собственно, только по этому и нужно то, что я предлагаю.
Я не использую макросы и вам не советую.

W>4. То, что вы предлагаете, по сути, не шаблон виртуальной функции, а автоматизация перегрузки виртуальных функций (которая, и без автоматизации в С++ работает плохо) с перекладыванием части работы на линковщик (что тоже явно С++ придется не по вкусу).

А что же тогда шаблон виртуальной функции?
И каждый день — без права на ошибку...
Re[7]: шаблонный виртуальный метод
От: Went  
Дата: 30.06.15 16:52
Оценка: 1 (1)
Здравствуйте, 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;
}


Вот как-то так. Да, такой подход не дает подсовывать "внезапные" члены в классы на этапе линковки, но при этом ломает на порядок меньше дров и имеет немало других полезных применений.
Re[8]: шаблонный виртуальный метод
От: B0FEE664  
Дата: 30.06.15 17:12
Оценка:
Здравствуйте, 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>Теперь мы можем определять в классе А и его наследниках обычный шаблон, и автоматически размножать его для любой реальной имплементации:

Это другая тема для других задач.
И каждый день — без права на ошибку...
Re[5]: шаблонный виртуальный метод
От: ononim  
Дата: 01.07.15 15:51
Оценка:
BFE>Вроде бы ничего экстраординарного. Я что-то упускаю? (ну, кроме dll и so, конечно)
Отличное "кроме". Куча софта имеет плагиновую структуру, в которой разные модули, скомпилированные в разные времена и даже разными компиляторами, инстансциирют наследников абстрактных классов. Я уж не говорю про COM и всю бороду которая на нем висит.
Как дополнительная плюшка — типа метавиртуальная функция — всегда пажалста, но существующий механизм виртуальных функций — вещь незаменимая.
Как много веселых ребят, и все делают велосипед...
Re[5]: шаблонный виртуальный метод
От: watchmaker  
Дата: 01.07.15 16:42
Оценка: 1 (1)
Здравствуйте, 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 для такого рода треша)? Но даже если мы договоримся, что чинить должен тот, кто последний сломал, то всё равно сама починка может быть слишком сложной — как уже заметили выше
Автор: Went
Дата: 30.06.15
, совершенно не нет гарантий, что colored_logger::write<MyNewType> = default вообще скомпилируется. Для обычных шаблонов есть SFINAE и возможность задать поведение в этом случае, для мультиметодов есть возможность обобщения, когда при отсутствии специализации происходит откат к более общим правилам, а тут встаёт жёсткое требование — нужно определить поведение для всего декартово произведения классов и шаблонных типов — и никак иначе, даже если многие комбинации будут откровенно абсурдными и в которых ничего разумного кроме throw bad_type сделать не останется.
Кажется, что это слишком сильное требование чтобы использование было по прежнему удобным. Хотя, в каких-то случаях, вроде строго вертикального наследования в изолированной части программы, наверное сойдёт.
Re[8]: шаблонный виртуальный метод
От: B0FEE664  
Дата: 03.07.15 14:00
Оценка:
Здравствуйте, 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

и подключать его по мере необходимости.
И каждый день — без права на ошибку...
Re[6]: шаблонный виртуальный метод
От: B0FEE664  
Дата: 03.07.15 14:09
Оценка:
Здравствуйте, 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 для такого рода треша)? Но даже если мы договоримся, что чинить должен тот, кто последний сломал, то всё равно сама починка может быть слишком сложной — как уже заметили выше
Автор: Went
Дата: 30.06.15
,


Это довольно просто упростить.
Автор: B0FEE664
Дата: 03.07.15


W> совершенно не нет гарантий, что colored_logger::write<MyNewType> = default вообще скомпилируется.

W>Для обычных шаблонов есть SFINAE и возможность задать поведение в этом случае, для мультиметодов есть возможность обобщения, когда при отсутствии специализации происходит откат к более общим правилам, а тут встаёт жёсткое требование — нужно определить поведение для всего декартово произведения классов и шаблонных типов — и никак иначе, даже если многие комбинации будут откровенно абсурдными и в которых ничего разумного кроме throw bad_type сделать не останется.

А почему, собственно, SFINAE не будет работать? Что этому помешает?
И каждый день — без права на ошибку...
Re[7]: шаблонный виртуальный метод
От: watchmaker  
Дата: 03.07.15 19:50
Оценка:
Здравствуйте, 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> в одной части программы не вызывало необходимости наличия такой же специализации у наследника в другой части программы (если программист честно пообещает, что это части никак не связаны).
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.