Возник теоретический спор об эквивалентности шаблонного и динамического полиморфизма в C++
Был предложен такой контрпример тезису об эквивалентности как виртуальный конструктор:
class obj
{
public:
virtual int foo (int) = 0;
};
class A : public obj;
class B : public obj;
...
/* returns A is config have 'a', B is 'b', and so on */
obj * getfromconfig (const char *filename);
int
entry (obj *x)
{
return x->foo();
}
int
main (void)
{
return entry (getfromconfig("my.xml"))
}
Как бы вы переписали этот код с шаблонным полиморфизмом вместо виртуальных функций?
Здравствуйте, Tilir, Вы писали:
T>Как бы вы переписали этот код с шаблонным полиморфизмом вместо виртуальных функций?
Никак, если имя файла задаётся в рантайм.
Здравствуйте, Tilir, Вы писали:
T>Hi,
T>Возник теоретический спор об эквивалентности шаблонного и динамического полиморфизма в C++
T>Был предложен такой контрпример тезису об эквивалентности как виртуальный конструктор:
T>
T>class obj
T>{
T>public:
T> virtual int foo (int) = 0;
T>};
T>class A : public obj;
T>class B : public obj;
T>...
T>/* returns A is config have 'a', B is 'b', and so on */
T>obj * getfromconfig (const char *filename);
T>int
T>entry (obj *x)
T>{
T> return x->foo();
T>}
T>int
T>main (void)
T>{
T> return entry (getfromconfig("my.xml"))
T>}
T>
T>Как бы вы переписали этот код с шаблонным полиморфизмом вместо виртуальных функций?
T>--- T>With best regards, Konstantin
-1 за main(void) в С++
чтоб два раза не вставать — в коде нет реализации getfromconfig, которая содержит наиболее интересную часть кода.
Здравствуйте, Abyx, Вы писали:
A>чтоб два раза не вставать — в коде нет реализации getfromconfig, которая содержит наиболее интересную часть кода.
Я же написал коммент. Очевидный фабричный метод:
/* returns A if config have 'a', B is 'b', and so on */
obj *
getfromconfig (const char *filename)
{
fstream f (filename, ios_base::in);
if (existsin (f, 'a'))
return new A();
if (existsin (f, 'b'))
return new B();
...
}
Здравствуйте, Kernan, Вы писали:
K>Никак, если имя файла задаётся в рантайм.
Если хочется именно в компайлтайм, то что-то вроде этого. Но смысл? то, чего ты хочешь, ты не получишь.
struct obj
{
public:
virtual int foo () = 0;
};
struct A : public obj
{
virtual int foo () {std::cout<<"FOO::A"<<std::endl; return 0;}
};
struct B : public obj
{
virtual int foo () {std::cout<<"FOO::B"<<std::endl; return 0;}
};
template<typename T>
struct entryByFile: public T
{
int entry()
{
return T::make()->foo();
}
};
struct fileA
{
obj* make() {return new A;}
};
struct fileB
{
obj* make() {return new B;}
};
int
main (void)
{
return entryByFile<fileB>().entry();
}
Здравствуйте, Kernan, Вы писали:
K>>Никак, если имя файла задаётся в рантайм. K>Если хочется именно в компайлтайм, то что-то вроде этого. Но смысл? то, чего ты хочешь, ты не получишь.
struct obj
{
public:
virtual int foo () = 0;
};
Хочется вообще избавиться от виртуальных функций, в чистом виде заменив рантайм-полиморфизм шаблонным. В этом варианте она все таки осталась.
Здравствуйте, Tilir, Вы писали:
T>Хочется вообще избавиться от виртуальных функций, в чистом виде заменив рантайм-полиморфизм шаблонным. В этом варианте она все таки осталась.
Тогда так. Но в рантайме это будет работать одинаково т.к. параметризовать можно только до компиляции кода. Т.е. ты не получаешь полиморфного поведения в рантайме.
template<typename T>
struct entryByFile: public T
{
int entry()
{
return T::foo();
}
};
struct fileA
{
int foo() {std::cout<<"DO something"<<std::endl; return 0;}
};
struct fileB
{
int foo() {std::cout<<"DO nothing"<<std::endl; return 0;}
};
int main()
{
return entryByFile<fileB>().foo();
}
T>class obj
T>{
T>public:
T> virtual int foo (int) = 0;
T>};
T>class A : public obj;
T>class B : public obj;
T>...
T>/* returns A if config have 'a', B is 'b', and so on */
T>obj *
T>getfromconfig (const char *filename)
T>{
T> fstream f (filename, ios_base::in);
T> if (existsin (f, 'a'))
T> return new A();
T> if (existsin (f, 'b'))
T> return new B();
T> ...
T>}
T>int
T>entry (obj *x)
T>{
T> return x->foo(); // тут, кситати, не верно метод вызываешь, или неверно описал я :xz:
T>}
T>int
T>main (void)
T>{
T> return entry (getfromconfig("my.xml"))
T>}
T>
T>Как бы вы переписали этот код с шаблонным полиморфизмом вместо виртуальных функций?
template<typename TObj> int entry( TObj* obj ) { return obj->foo(); }
int
main (void)
{
const char *filename = "my.xml";
fstream f (filename, ios_base::in);
if (existsin (f, 'a'))
return entry( new A );
if (existsin (f, 'b'))
return entry( new B );
...
}
Но смысл?
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Будет некая простыня. Пусть твой фабричный метод возвращает пару целочисленную переменную, задающую тип, и указатель (убыв бы) на сгенерированный тип, отнаследованный от базового. Тогда то, что ты хочешь выглядит примерно так.
(можно короче раза в два но не больше)
Здравствуйте, denisko, Вы писали:
D>Будет некая простыня. Пусть твой фабричный метод возвращает пару целочисленную переменную, задающую тип, и указатель (убыв бы) на сгенерированный тип, отнаследованный от базового. Тогда то, что ты хочешь выглядит примерно так.
Очень круто, я серьёзно. К сожалению при всей методологической полезности, организация RTTI руками в этом случае как раз и доказывает необходимость именно динамического полиморфизма.
Здравствуйте, Tilir, Вы писали: T>Хочется вообще избавиться от виртуальных функций, в чистом виде заменив рантайм-полиморфизм шаблонным.
Может я чего не так понял, но предположу вариант:
Скрытый текст
#include <iostream>
template<typename Tnext>
class Converter
{
public:
template<typename Tthis>
inline static Tnext* cast(Tthis* _this)
{
return static_cast<Tnext*>(_this);
}
};
template<typename Tfist, typename Tsecond>
class ChainConverter
{
public:
template<typename Tthis>
inline static auto cast(Tthis* _this)-> decltype (Tsecond::cast(Tfist::cast(_this)))
{
return Tsecond::cast(Tfist::cast(_this));
}
};
template<typename TConverter>
class obj
{
public:
typedef obj<TConverter> TObj;
public:
// предположим:
/*virtual*/int foo() /* = 0;*/
{ return TConverter::cast(this)->foo(); }
// чтобы сделать виртуальный вызов из наследников без приведенияint virtualFoo(){ return foo(); }
protected:
obj(){}
};
template<typename TConverter>
class Aimpl : public obj<ChainConverter<Converter<Aimpl<TConverter>>,TConverter>>
{
public:
typedef Aimpl<TConverter> TAimpl;
public:
int foo() {std::cout << "FOO::A" << std::endl; return 0; }
};
template<typename TConverter>
class Bimpl : public Aimpl<ChainConverter<Converter<Bimpl<TConverter>>, TConverter>>
{
public:
typedef Bimpl<TConverter> TBimpl;
public:
int foo() { std::cout << "FOO::B" << std::endl; return 0; }
};
template<typename TConverter>
class Cimpl : public Aimpl<ChainConverter<Converter<Cimpl<TConverter>>, TConverter>>
{
public:
typedef Cimpl<TConverter> TCimpl;
public:
int foo() { std::cout << "FOO::C" << std::endl; return 0; }
};
template<typename TConverter>
class Dimpl :
public Bimpl<ChainConverter<Converter<Dimpl<TConverter>>, TConverter>>,
public Cimpl<ChainConverter<Converter<Dimpl<TConverter>>, TConverter>>
{
public:
typedef Dimpl<TConverter> TDimpl;
public:
int foo() { std::cout << "FOO::D" << std::endl; return 0; }
};
class A : public Aimpl<Converter<A>>
{
};
class B : public Bimpl<Converter<B>>
{
};
class C : public Cimpl<Converter<C>>
{
};
class D : public Dimpl<Converter<D>>
{
};
int main()
{
A a;
a.foo();
a.virtualFoo();
B b;
B::TObj& ob = b;
B::TAimpl& aimpl = b;
ob.foo();
ob.virtualFoo();
b.foo();
b.virtualFoo();
aimpl.foo();
aimpl.virtualFoo();
std::cout << "--D--" << std::endl;
D d;
d.foo();
d.TBimpl::foo();
d.TBimpl::TAimpl::foo();
d.TCimpl::foo();
d.TCimpl::TAimpl::foo();
d.TBimpl::TObj::virtualFoo();
d.TCimpl::TObj::virtualFoo();
return 0;
}
/* Output:
FOO::A
FOO::A
FOO::B
FOO::B
FOO::B
FOO::B
FOO::A
FOO::B
--D--
FOO::D
FOO::B
FOO::A
FOO::C
FOO::A
FOO::D
FOO::D
// */
Здравствуйте, F3V, Вы писали:
F3V>Может я чего не так понял, но предположу вариант:
Тоже хороший вариант. Известна двойственность виртуальных функций и PImpl. Но по сути это такое же "RTTI своими силами", как и в случае http://rsdn.ru/forum/cpp/5728786.1
Такой подход скорее доказывает что динамический полиморфизм не сводится к шаблонному, раз уже нам пришлось по факту его воспроизводить, заводя ту же vtbl но руками.
int res;
getfromconfig("my.xml", [&](auto my) {
getfromconfig("his.xml", [&](auto his) {
res = entry(my,his);
})
});
К>>А чтоб удобнее пользоваться было, а не громоздить руками — придётся прикручивать карринг и комбинаторную логику.
T>Было бы очень интересно посмотреть хотя бы набросок.
Чуть попозже попробую формально подойти к этому.
Кажется, что std::bind на такое, в принципе, способен.
.
Если я его правильно понял, то он предложил перенести точку входа в программу во внутреннюю лямбду, где конфигурация уже определена.
Тогда не код будет управлять данными, а под данные будет подстраиваться код...
Если немного переделать, то получится примерно так:
Скрытый текст
#include <iostream>
#include <string>
#include <type_traits>
class A
{
public:
int foo() { std::cout << "FOO::A" << std::endl; return 0; }
};
class B
{
public:
int foo() { std::cout << "FOO::B" << std::endl; return 0; }
};
class C
{
public:
int foo() { std::cout << "FOO::C" << std::endl; return 0; }
};
class D
{
public:
int foo() { std::cout << "FOO::D" << std::endl; return 0; }
};
template<int AppRestartRequestCount>
class EmptyConfig
{
public:
static const int typesRemainsCount = 4;
static const int appRestartRequestCount = AppRestartRequestCount;
};
template<typename TParentConfig, typename T, typename Q = T>
class Config :public TParentConfig
{
public:
typedef T TType;
typedef TParentConfig TParent;
public:
static const int typesRemainsCount = TParentConfig::typesRemainsCount - 1;
};
template<typename TParentConfig, typename T>
class Config<TParentConfig, T, typename std::enable_if<TParentConfig::typesRemainsCount == 0, T>::type>
{
public:
typedef T TType;
typedef TParentConfig TParent;
public:
static const int typesRemainsCount = -1;
};
template<typename TLayeredConfig>
class FullConfig
{
public:
typedef typename TLayeredConfig::TType T4;
typedef typename TLayeredConfig::TParent::TType T3;
typedef typename TLayeredConfig::TParent::TParent::TType T2;
typedef typename TLayeredConfig::TParent::TParent::TParent::TType T1;
public:
static const int typesPackCount = EmptyConfig<0>::typesRemainsCount;
static const int appRestartRequestCount = TLayeredConfig::appRestartRequestCount;
};
template<typename TConfig>
class ReverseConfig
{
public:
typedef typename TConfig::T1 T4;
typedef typename TConfig::T2 T3;
typedef typename TConfig::T3 T2;
typedef typename TConfig::T4 T1;
static const int typesPackCount = TConfig::typesPackCount;
static const int appRestartRequestCount = TConfig::appRestartRequestCount - 1;
};
template<typename TConf>
class AppStop
{
public:
static int run()
{
return -1;
}
};
template<typename TConf>
class App
{
public:
static int run()
{
typename TConf::T1 t1;
typename TConf::T2 t2;
typename TConf::T3 t3;
typename TConf::T4 t4;
t1.foo();
t2.foo();
t3.foo();
t4.foo();
std::cout << "reverse reconfig.." << std::endl;
return std::conditional<TConf::appRestartRequestCount != 0, App<ReverseConfig<TConf>>, AppStop<TConf>>::type::run();
}
};
template<typename TConfig, template <typename> class TApp, typename Q = TConfig>
class Configurator
{
public:
static int configAndRun(const std::string& /*_s*/)
{
return TApp<FullConfig<TConfig>>::run();
}
};
template<typename TConfig, template <typename> class TApp>
class Configurator<TConfig, TApp,
typename std::enable_if<TConfig::typesRemainsCount != 0, TConfig>::type>
{
public:
static int configAndRun(const std::string& _s)
{
auto c = _s.at(0);
auto nextS = _s.substr(1);
switch (c)
{
default:
case'A':
return Configurator<Config<TConfig, A>, TApp>::configAndRun(nextS);
case'B':
return Configurator<Config<TConfig, B>, TApp>::configAndRun(nextS);
case'C':
return Configurator<Config<TConfig, C>, TApp>::configAndRun(nextS);
case'D':
return Configurator<Config<TConfig, D>, TApp>::configAndRun(nextS);
}
}
};
int main()
{
const char* configs[] = { "ABCD", "DABC" };
for (auto& cfg : configs)
{
std::cout << "main config: " << cfg << std::endl;
Configurator<EmptyConfig<1>, App>::configAndRun(cfg);
}
return 0;
}
/* Output:
main config: ABCD
FOO::A
FOO::B
FOO::C
FOO::D
reverse reconfig..
FOO::D
FOO::C
FOO::B
FOO::A
reverse reconfig..
main config: DABC
FOO::D
FOO::A
FOO::B
FOO::C
reverse reconfig..
FOO::C
FOO::B
FOO::A
FOO::D
reverse reconfig..
*/
Здравствуйте, Tilir, Вы писали:
T>Это читерство.
Это не читерство, а статический полиморфизм так работает...
T>Идея контрпримера была в том, что getfromconfig используется не только в entry, а где угодно.
Ну так и пишешь это всё "где угодно" шаблонным, потом параметризуешь так и сяк, а вместо конфига можно просо нужную версию ставить, например, или в мэйне выбирать...
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
#include <cstdio>
#include <vector>
#include <functional>
using namespace std;
using namespace std::placeholders;
// семейство типов ABCDvoid show(char x) { printf("%c ", x); }
struct A { int foo() { show('A'); return 1; } } const a;
struct B { unsigned foo() { show('B'); return 2; } } const b;
struct C { long foo() { show('C'); return 3; } } const c;
struct D { double foo() { show('D'); return 4; } } const d;
// все наши полиморфные функции будут иметь первоклассный фиксированный типstruct getconfig_t // void g(char,K)
{
template<class K> // где K имеет сигнатуру void f(ABCD)void operator()(char t, K kont) const
{
switch(t)
{
case'a': kont(A()); break;
case'b': kont(B()); break;
case'c': kont(C()); break;
case'd': kont(D()); break;
default: printf("exception %c\n", t); break; // исключение состоит в том, чтобы не вызывать продолжение :)
}
}
} const getconfig;
struct entry_t // void e(ABCD,ABCD,K)
{
template<class T1, class T2, class K> // где T1 и T2 это ABCD, а K имеет сигнатуру void f(NUM)void operator()(T1 t1, T2 t2, K kont) const
{
kont(t1.foo()*10 + t2.foo()); // будем изучать правила продвижения числовых типов
}
} const entry;
struct finish_t // void f(NUM)
{
void operator()(int n) const { printf("int %d\n", n); }
void operator()(unsigned n) const { printf("uint %u\n", n); }
void operator()(long n) const { printf("long %ld\n", n); }
void operator()(unsigned long n) const { printf("ulong %lu\n", n); }
void operator()(double n) const { printf("double %g\n", n); }
} const finish;
// протектор, аналогичный boost::protect
// (его нет в стандартной библиотеке, поэтому пришлось стырить из интернета рецепт)template<typename T>
struct protect_wrapper : T
{
protect_wrapper(const T& t) : T(t)
{
}
protect_wrapper(T&& t) : T(std::move(t))
{
}
};
template<typename T>
typename std::enable_if< !std::is_bind_expression< typename std::decay<T>::type >::value,
T&& >::type
protect(T&& t)
{
return std::forward<T>(t);
}
// нам потребуется только эта веткаtemplate<typename T>
typename std::enable_if< std::is_bind_expression< typename std::decay<T>::type >::value,
protect_wrapper<typename std::decay<T>::type > >::type
protect(T&& t)
{
return protect_wrapper<typename std::decay<T>::type >(std::forward<T>(t));
}
// устойчивая идиома: изоляция bind-выраженияtemplate<class... Args>
auto probind(Args... args) -> decltype(protect(bind(args...))) { return protect(bind(args...)); }
// карринг двухместной функцииstruct curry_t
{
template<class F, class X>
auto operator()(F&& f, X&& x) const -> decltype( probind(f,x,_1) ) { return probind(f,x,_1); }
} const curry;
// комбинируем!void run(char x, char y)
{
// кирпичики: продолженияauto g = protect(bind(getconfig, x, _1)); // void(K)auto h = protect(bind(getconfig, y, _1)); // void(K)auto e = protect(bind(entry, _1, _2, _3)); // void(ABCD,ABCD,K)
// последний кирпичик - без продолженияauto f = finish;
// комбинацииauto fe = probind(e,_1,_2,f);
auto feh = probind(h, bind(curry,fe,_1));
auto fegh = probind(g, feh);
// запускаем
fegh();
}
int main()
{
vector<char> abcd = { 'a','b','c','d','e' };
for(auto x : abcd)
for(auto y : abcd)
run(x,y);
}
Карринг здесь нужен для того, чтобы разнести первый и второй аргументы замыкания fe на разные этажи bind-выражения.
Если просто написать probind(h, bind(fe,_1,_2)) — то получим двухместное замыкание, в котором h получает на вход конечный результат fe(_1,_2)
Если вместо bind сделать probind(h, probind(fe,_1,_2)) — то получим нульместное замыкание, в котором h получает двухместное.
А нам нужно одноместное и одноместное.
В стандартной библиотеке есть комбинатор bind1st, но, к сожалению, это шаблон функции.
К>// карринг двухместной функции
К>struct curry_t
К>{
К> template<class F, class X>
К> auto operator()(F&& f, X&& x) const -> decltype( probind(f,x,_1) ) { return probind(f,x,_1); }
К>} const curry;
// более универсальное решение - расслаивание аргументов на немедленно подставленные и идущие в составе синтаксического дерева bindstruct _1_t {} const _1_; auto _pass_(_1_t) -> decltype(_1) { return _1; }
struct _2_t {} const _2_; auto _pass_(_2_t) -> decltype(_2) { return _2; }
struct _3_t {} const _3_; auto _pass_(_3_t) -> decltype(_3) { return _3; }
template<class T> auto _pass_(T&& t) -> T&& { return t; }
struct partial_t
{
template<class F>
auto operator()(F&& f) const -> decltype( probind(f) )
{ return probind(f); }
template<class F, class T1>
auto operator()(F&& f, T1&& t1) const -> decltype( probind(f, _pass_(t1)) )
{ return probind(f, _pass_(t1)); }
template<class F, class T1, class T2>
auto operator()(F&& f, T1&& t1, T2&& t2) const -> decltype( probind(f, _pass_(t1), _pass_(t2)) )
{ return probind(f, _pass_(t1), _pass_(t2)); }
} const partial;
К>// комбинируем!
К>void run(char x, char y)
К>{
К> // кирпичики: продолжения
К> auto g = protect(bind(getconfig, x, _1)); // void(K)
К> auto h = protect(bind(getconfig, y, _1)); // void(K)
К> auto e = protect(bind(entry, _1, _2, _3)); // void(ABCD,ABCD,K)
К> // последний кирпичик - без продолжения
К> auto f = finish;
К> // комбинации
К> auto fe = probind(e,_1,_2,f);
auto feh = probind(h, bind(partial,fe, _1_, _1));
К> auto fegh = probind(g, feh);
К> // запускаем
К> fegh();
К>}
К>
Здесь _1 является формальным аргументом замыкания feh и будет подставлен при вызове его из h
а _1_ — аргументом того замыкания, которое будет возвращено функцией partial.