В продолжение
CRTP v2.0Автор: remark
Дата: 28.09.07
.
Что можно ещё с этим сделать?
Допустим есть базовый библиотечный класс, от которого пользователи должны наследовать свои классы (за примерами далеко ходить не надо, практически каждая С++ библиотека это практикует).
Допустим в пользовательских классах должны быть функции init()/fini(), которые должны вызываться соответственно в "порядке конструкторов" и в "порядке деструкторов" (т.е. от базового класса к производным или от производного к базовому)
"Классический" подход поразумевает следующую
фигнюАвтор: remark
Дата: 29.04.06
Что делаем мы:
struct null_t {};
typedef void(null_t::*mf_t)();
struct CRTP_base
{
void init(mf_t f) {}
void fini(mf_t f) {}
};
template<typename derived_t, typename base_t = CRTP_base>
struct CRTP : base_t
{
void init(mf_t f = 0)
{
mf_t ff = (mf_t)&derived_t::do_init;
this->base_t::init(ff);
// проверяем на предмет, что в классе не определена функция do_init
if (ff != f) static_cast<derived_t*>(this)->do_init();
}
void fini(mf_t f = 0)
{
mf_t ff = (mf_t)&derived_t::do_fini;
// проверяем на предмет, что в классе не определена функция do_fini
if (ff != f) static_cast<derived_t*>(this)->do_fini();
this->base_t::fini(ff);
}
};
struct A : CRTP<A>
{
void do_init() {std::cout << __FUNCTION__ << std::endl;}
void do_fini() {std::cout << __FUNCTION__ << std::endl;}
};
struct B : CRTP<B, A>
{
void do_init() {std::cout << __FUNCTION__ << std::endl;}
void do_fini() {std::cout << __FUNCTION__ << std::endl;}
};
struct C : CRTP<C, B>
{
void do_init() {std::cout << __FUNCTION__ << std::endl;}
void do_fini() {std::cout << __FUNCTION__ << std::endl;}
};
void test()
{
A().init();
A().fini();
std::cout << std::endl;
B().init();
B().fini();
std::cout << std::endl;
C().init();
C().fini();
std::cout << std::endl;
}
Вывод:
A::do_init
A::do_fini
A::do_init
B::do_init
B::do_fini
A::do_fini
A::do_init
B::do_init
C::do_init
C::do_fini
B::do_fini
A::do_fini
Заккоментируем методы B::do_init()/B::do_fini(), тогда вывод:
A::do_init
A::do_fini
A::do_init
A::do_fini
A::do_init
C::do_init
C::do_fini
A::do_fini
В производных классах надо только определить нужную функцию, всё остальное делает фреймворк, в т.ч. определяет в каком порядке вызывать функции производных классов.
Идём дальше. Допустим надо сделать не "порядок конструкторов" или "порядок деструкторов", а произвольный порядок, что бы можно было написать что-то типа такого:
class Some
{
//...
void some(handler_t inner, some_param_t& some_param)
{
// some object on stack
mutex_lock l (m);
try
{
some_preaction();
// call function from derived class here!
inner(); // <-------------------------------------
some_postaction();
}
catch (...)
{
handle_exceptions();
}
}
//...
};
И в отличие от виртуальных функций мы хотим что бы управление шло "сверху вниз", т.е. вначале вызывается функция базового класса, потом она вызывает функцию производного и т.д., а не наоборот. (хотя можно сделать и наоборот)
Вот код, реализующий это:
#include <string>
#include <iostream>
#include <boost/function.hpp>
#include <boost/bind.hpp>
typedef boost::function<void()> handler_t;
struct null_t {};
typedef void (null_t::*fp)();
void null_f() {}
struct msg_base
{
int i;
};
struct dialog_base;
template<typename derived_t, typename base_t = dialog_base>
struct dialog : public base_t
{
template<typename msg_t>
handler_t create_handler(handler_t h, msg_t& m, fp p)
{
// проверяем, определена ли в derived_t функция on_msg()
void (derived_t::*f)(handler_t, msg_t&) = &derived_t::on_msg;
if (p == (fp)f)
// если нет, то просто передаём управление базовому классу
return base_t::create_handler(h, m, (fp)f);
// если определена, то создаём для неё функтор, и передаём в базовый класс уже его
handler_t hh = boost::bind(f, static_cast<derived_t*>(this), h, boost::ref(m));
return base_t::create_handler(hh, m, (fp)f);
}
template<typename msg_t>
void handle_msg(msg_t& m)
{
// рекурсивно оборачиваем пустой функтор в функции on_msg
handler_t hh = create_handler(handler_t(null_f), m, 0);
// вызываем
hh();
}
};
// самый базовый не шаблонный класс
struct dialog_base
{
template<typename msg_t>
handler_t create_handler(handler_t h, msg_t& m, fp p)
{
// здесь прерываем рекурсию по созданию функторов обработки
return h;
}
template<typename msg_t>
void on_msg(handler_t h, msg_t& m)
{
// здесь просто вызываем функтор
h();
}
};
// далее пользовательские диалоги и сообщения
struct base : dialog<base>
{
// обработчик для всех сообщений
template<typename msg_t>
void on_msg(handler_t h, msg_t& m)
{
std::cout << __FUNCTION__ << " - " << m.i++ << std::endl;
h();
}
};
struct derived1 : dialog<derived1, base>
{
// обработчик для всех сообщений
template<typename msg_t>
void on_msg(handler_t h, msg_t& m)
{
std::cout << __FUNCTION__ << " - " << m.i++ << std::endl;
h();
}
};
struct my_msg1 : msg_base
{
int x;
my_msg1()
{
i = 100;
x = 200;
}
};
struct my_msg2 : msg_base
{
std::string y;
my_msg2()
{
i = 500;
y = "qwerty";
}
};
struct derived2 : dialog<derived2, derived1>
{
// обработчик для всех сообщений
template<typename msg_t>
void on_msg(handler_t h, msg_t& m)
{
std::cout << __FUNCTION__ << " - " << m.i++ << std::endl;
h();
}
// а здесь определяем обработчик для конкретного типа сообщения,
// соотв. имеем доступ к его членам
void on_msg(handler_t h, my_msg2& m)
{
std::cout << __FUNCTION__ << "(my_msg2) - " << m.i++ << " - " << m.y << std::endl;
h();
}
};
struct derived3 : dialog<derived3, derived2>
{
// обработчик для всех сообщений
template<typename msg_t>
void on_msg(handler_t h, msg_t& m)
{
std::cout << __FUNCTION__ << " - " << m.i++ << std::endl;
h();
}
// а здесь определяем обработчик для конкретного типа сообщения,
// соотв. имеем доступ к его членам
void on_msg(handler_t h, my_msg1& m)
{
std::cout << __FUNCTION__ << "(my_msg1) - " << m.i++ << " - " << m.x << std::endl;
h();
}
};
int main()
{
derived2().handle_msg(my_msg1());
std::cout << std::endl;
derived3().handle_msg(my_msg1());
std::cout << std::endl;
derived3().handle_msg(my_msg2());
std::cout << std::endl;
}
Вывод:
base::on_msg — 100
derived1::on_msg — 101
derived2::on_msg — 102
base::on_msg — 100
derived1::on_msg — 101
derived2::on_msg — 102
derived3::on_msg(my_msg1) — 103 — 200
base::on_msg — 500
derived1::on_msg — 501
derived2::on_msg(my_msg2) — 502 — qwerty
derived3::on_msg — 503
Вариаций можно ещё много придумать, но основную идею я показал. Далее можно затачивать это под конкретные нужды.
Здравствуйте, remark, Вы писали:
Идея интересная
, но я бы предпочел без хаков
:
template<typename T, T f1, T f2>
struct not_eq
{
static bool const result = true;
};
template<typename T, T f>
struct not_eq<T, f, f>
{
static bool const result = false;
};
void init()
{
this->base_t::init();
if (not_eq<void (derived_t::*)(), &base_t::do_init, &derived_t::do_init>::result)
static_cast<derived_t*>(this)->do_init();
}
Здравствуйте, Quasi, Вы писали:
Q>//Единственное, сделать compile-time константой не получится
Q>//static const bool doPresent = (&base_t::do_init != &derived_t::do_init);
Не успел в своё время допостить всё, что собирался. Время пришло
Переводим проверку наличия функции в потомке в компайл-тайм:
template<mf_t f1, mf_t f2>
struct not_eq
{
static bool const result = true;
};
template<mf_t f>
struct not_eq<f, f>
{
static bool const result = false;
};
void init_impl()
{
this->base_t::init_impl();
if (not_eq<(mf_t)&derived_t::do_init, (mf_t)&base_t::do_init>::result)
static_cast<derived_t*>(this)->do_init();
}
Смотрим что получилось:
void init()
{
004047D0 push esi
004047D1 mov esi,ecx
004047D3 call A::do_init (404740h)
004047D8 mov ecx,esi
004047DA call B::do_init (404770h)
004047DF mov ecx,esi
004047E1 pop esi
004047E2 jmp C::do_init (4047A0h)
}
Даже tail-вызов соптимизировал! Ай молодец!
Комментируем функцию B::do_init():
void init()
{
00401470 push esi
00401471 mov esi,ecx
00401473 call A::do_init (401410h)
00401478 mov ecx,esi
0040147A pop esi
0040147B jmp C::do_init (401440h)
}
Очевидно, что виртуальные функции близко не валялись.
Здравствуйте, remark, Вы писали:
R>В продолжение CRTP v2.0Автор: remark
Дата: 28.09.07
.
R>Что можно ещё с этим сделать?
1) А что делать, если таки нужен результат функции?
2) Для случая "произвольного порядка", ИМХО, проще писать это традиционным способом...
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Здравствуйте, Аноним, Вы писали:
А>Здравствуйте, remark, Вы писали:
А>Насколько законно данное приведение "mf_t ff = (mf_t)&derived_t::do_init"?
А>Можно ссылку на стандарт
5.2.10
The mapping performed by reinterpret_cast is implementation-defined.
...
An rvalue of type “pointer to member of X of type T1” can be explicitly converted to an rvalue of type
“pointer to member of Y of type T2” if T1 and T2 are both function types or both object types.66) The null
member pointer value (4.11) is converted to the null member pointer value of the destination type. The
result of this conversion is unspecified, except in the following cases:
— converting an rvalue of type “pointer to member function” to a different pointer to member function
type and back to its original type yields the original pointer to member value.
...
Здравствуйте, remark, Вы писали:
R>Здравствуйте, Аноним, Вы писали:
А>>Здравствуйте, remark, Вы писали:
А>>Насколько законно данное приведение "mf_t ff = (mf_t)&derived_t::do_init"?
А>>Можно ссылку на стандарт
5.2.10
The mapping performed by reinterpret_cast is implementation-defined.
...
An rvalue of type “pointer to member of X of type T1” can be explicitly converted to an rvalue of type
“pointer to member of Y of type T2” if T1 and T2 are both function types or both object types.66) The null
member pointer value (4.11) is converted to the null member pointer value of the destination type. The
result of this conversion is unspecified, except in the following cases:
— converting an rvalue of type “pointer to member function” to a different pointer to member function
type and back to its original type yields the original pointer to member value.
...
Тогда результат выражения "if(ff != f)" тоже unspecified
А вообще, кто-нибудь объяснит, как такое приведение реализовано — размеры указателей могут отличаться
Здравствуйте, Аноним, Вы писали:
А>Здравствуйте, remark, Вы писали:
R>>Здравствуйте, Аноним, Вы писали:
А>>>Здравствуйте, remark, Вы писали:
А>>>Насколько законно данное приведение "mf_t ff = (mf_t)&derived_t::do_init"?
А>>>Можно ссылку на стандарт
А>А>5.2.10
А>The mapping performed by reinterpret_cast is implementation-defined.
А>...
А>An rvalue of type “pointer to member of X of type T1” can be explicitly converted to an rvalue of type
А>“pointer to member of Y of type T2” if T1 and T2 are both function types or both object types.66) The null
А>member pointer value (4.11) is converted to the null member pointer value of the destination type. The
А>result of this conversion is unspecified, except in the following cases:
А>— converting an rvalue of type “pointer to member function” to a different pointer to member function
А>type and back to its original type yields the original pointer to member value.
А>...
А>Тогда результат выражения "if(ff != f)" тоже unspecified
А>А вообще, кто-нибудь объяснит, как такое приведение реализовано — размеры указателей могут отличаться
Ne putay "unspecified" i "undefined"!
Ya polagayus' tol'ko chto "mapping" odnoznachniy. T.e. odno i tozhe znachenie perehodit d odin i tozhe rezul'tat.
"mapping" voobshe mozhet bit' neodnoznachnim?
Здравствуйте, remark, Вы писали:
Не понял, а зачем такой замут с приведением к mf_t?
Когда можно просто:
struct CRTP_base
{
void init() {}
void fini() {}
void do_init(){}
void do_fini(){}
};
template<typename derived_t, typename base_t = CRTP_base>
struct CRTP : base_t
{
void init()
{
this->base_t::init();
if (&base_t::do_init != &derived_t::do_init)
static_cast<derived_t*>(this)->do_init();
}
void fini()
{
if (&base_t::do_init != &derived_t::do_init)
static_cast<derived_t*>(this)->do_fini();
this->base_t::fini();
}
//Единственное, сделать compile-time константой не получится
//static const bool doPresent = (&base_t::do_init != &derived_t::do_init);
};
R>
Опшибся
struct CRTP_base
{
void init() {}
void fini() {}
void do_init(){}
void do_fini(){}
};
template<typename derived_t, typename base_t = CRTP_base>
struct CRTP : base_t
{
void init()
{
this->base_t::init();
if (&base_t::do_init != &derived_t::do_init)
static_cast<derived_t*>(this)->do_init();
}
void fini()
{
if (&base_t::do_fini != &derived_t::do_fini)
static_cast<derived_t*>(this)->do_fini();
this->base_t::fini();
}
//Единственное, сделать compile-time константой не получится
//static const bool doPresent = (&base_t::do_init != &derived_t::do_init);
};
я так понял, что если в конструкторе CRTP сделать вызов init, то получим то,что нельзя сделать с помощью виртуальных функций — вызов виртуальной функции из конструктора фреймворка
Приколько, еще бы придумать способ чтобы в это время конечный объект был уже сконструирован