#c #templates #template-specialization
#c #шаблоны #шаблон-специализация
Вопрос:
Я хочу определить макрос, который можно вызывать в разных местах (в области видимости файла), чтобы создавать функции, которые что-то делают. (В примере ниже функции просто печатают сообщение, но, конечно, мое реальное намерение — сделать некоторые другие полезные вещи.) Проблема в том, что я хочу, чтобы какая-нибудь функция «manager» (в моем примере это будет просто main()
) каким-то образом успешно вызывала их все (в любом порядке) без какой-либо зависимости кода от вызовов макросов (за исключением самих вызовов макросов, конечно). Я имею в виду, что как только файл будет записан, другой программист сможет просто вставить несколько новых вызовов макроса в разных местах или удалить некоторые из существующих вызовов, и код все равно будет работать без каких-либо дальнейших изменений. Я понимаю, что это может быть сделано с использованием статических объектов, но я хочу изучить другой подход. Я собираюсь использовать некоторые хитрости с шаблоном и тот факт, что __LINE__
он монотонно увеличивается.
#include <iostream>
using namespace std;
template<int i>
inline void f()
{
f<i-1>();
}
#define START_REGISTRATION
template<>
inline void f<__LINE__>() {} /* stop the recursion */
template<> void f<__LINE__>() /* force semicolon */
#define REGISTER(msg)
template<>
inline void f<__LINE__>()
{
cout << #msg << endl;
f<__LINE__ - 1>();
}
template<> void f<__LINE__>() /* force semicolon */
// Unrelated code ...
START_REGISTRATION;
// Unrelated code ...
REGISTER(message 1);
// Unrelated code ...
REGISTER(message 2);
// Unrelated code ...
REGISTER(message 3);
// Unrelated code ...
// manager function (in this case main() )
int main()
{
f<__LINE__>();
}
Это выводит
message 3
message 2
message 1
как и ожидалось.
У этого решения есть несколько недостатков.
- Не удается вызвать
REGISTER
дважды в одной строке. - Произойдет сбой, если с
#line
будет воспроизведен. - Необходимо поместить диспетчер после всех вызовов
REGISTER
. - Увеличенное время компиляции из-за рекурсивных экземпляров.
- Если все «фиктивные» экземпляры
f
не будут встроены, глубина стека вызовов во время выполнения будет такой же большой, как количество строк междуSTART_REGISTRATION;
иf<__LINE__>();
в диспетчере. - Раздувание кода: если все «фиктивные» экземпляры
f
не будут встроены, количество экземпляров будет аналогичным образом таким же большим. - Чрезмерная глубина рекурсии при создании экземпляра, вероятно, достигнет предела компилятора (500 по умолчанию в моей системе).
Проблемы 1-4 Я действительно не возражаю. Проблему 5 можно устранить, заставив каждую функцию возвращать указатель на предыдущую, а диспетчер использовать эти указатели для итеративного вызова функций вместо того, чтобы они вызывали друг друга. Проблему 6 можно устранить, создав аналогичную конструкцию шаблона класса, которая способна вычислять для каждого вызова REGISTER
того, какая функция была создана в предыдущем вызове, и, таким образом, создавать экземпляры только тех функций, которые действительно что-то делают. Затем избыточное создание экземпляра будет перенесено из шаблона функции в шаблон класса, но создание экземпляров класса будет только обременять компилятор; они не вызовут никакой генерации кода. Итак, моя реальная проблема — проблема 7, и вопрос в следующем: есть ли способ реструктурировать вещи так, чтобы компилятор выполнял создание экземпляров итеративно, а не рекурсивно. Я также открыт для различных подходов в целом (кроме тех, которые связаны со статическими объектами). Одно простое решение — сгруппировать все регистрации вместе прямо перед менеджером (или добавить STOP_REGISTRATION
макрос, чтобы завершить блок регистраций), но это нарушило бы значительную часть моей цели (регистрация материала рядом с кодом, который его определяет).
Редактировать: Было несколько интересных предложений, но, боюсь, я недостаточно ясно выразился в терминах того, чего я надеюсь достичь. Меня действительно интересуют две вещи: решение поставленной проблемы (т. Е. Отсутствие статики, одна строка на регистрацию, никаких дополнительных изменений при добавлении / удалении регистраций и, хотя я забыл сказать об этом, только стандартный C — следовательно, никакого повышения). Как я сказал в комментариях ниже, мой интерес носит скорее теоретический характер: я надеюсь изучить некоторые новые методы. Следовательно, я действительно хотел бы сосредоточиться на реструктуризации вещей, чтобы исключить рекурсию (или, по крайней мере, уменьшить) или найти другой подход, который удовлетворяет ограничениям, которые я изложил выше.
Правка 2: решение MSalter является большим шагом вперед. Сначала я думал, что каждая регистрация повлечет за собой полную стоимость строк до нее, но потом я понял, что, конечно, экземпляр функции может быть создан только один раз, поэтому с точки зрения создания экземпляров мы платим ту же цену, что и при линейном поиске, но глубина рекурсии становится логарифмической. Если у меня до этого дойдет время, я опубликую полное решение, устраняющее проблемы 5-7. Тем не менее, было бы неплохо посмотреть, можно ли это выполнить с постоянной глубиной рекурсии, и лучше всего, с количеством экземпляров, линейным по количеству вызовов (аналогично решению boost).
Правка 3: Вот полное решение.
#define START_REGISTRATION
template<int lo, int hi>
struct LastReg {
enum {
LINE_NUM = LastReg<(lo hi)/2 1, hi>::LINE_NUM ?
static_cast<int>(LastReg<(lo hi)/2 1, hi>::LINE_NUM) :
static_cast<int>(LastReg<lo, (lo hi)/2>::LINE_NUM)
};
};
template<int l>
struct LastReg<l, l> {
enum { LINE_NUM = 0 };
};
template<int l>
struct PrevReg {
enum { LINE_NUM = LastReg<__LINE__ 1, l - 1>::LINE_NUM };
};
template<int l> void Register() {}
template<int l> void Register() /* force semicolon */
#define REGISTER(msg)
template<>
struct LastReg<__LINE__, __LINE__> {
enum { LINE_NUM = __LINE__ };
};
template<>
void Register<__LINE__>()
{
cout << __LINE__ << ":" << #msg << endl;
Register<PrevReg<__LINE__>::LINE_NUM>();
}
template<> void Register<__LINE__>() /* force semicolon */
#define END_REGISTRATION
void RegisterAll()
{
Register<PrevReg<__LINE__>::LINE_NUM>();
}
void RegisterAll() /* force semicolon */
START_REGISTRATION;
REGISTER(message 1);
REGISTER(message 2);
END_REGISTRATION;
int main()
{
RegisterAll();
}
Комментарии:
1. Мне все еще неясно, какой именно результат требуется ? Если вы сможете описать в 2-3 строки в конце, то это упростит задачу.
2. Не приведет ли функция
void f<__LINE__>()
к бесконечному вызову? Потому что вы не проверяете, меньше или равно нулю переданный номер строки? Или вы пропустили включение этого в показанный здесь код?3. Можете ли вы пояснить, зачем вам нужен рекурсивный шаг? Я не вижу, что вам это нужно, или вам?
4. @Vite:
__LINE__
является значением, введенным препроцессором в строку в файле, поэтому я не вижу необходимости проверять__LINE__ <= 0
5. вы можете установить максимальный уровень рекурсии равным N с помощью -ftemplate-depth-N (по крайней мере, в g )
Ответ №1:
Проблема, с которой вы сталкиваетесь, заключается в том, что вы выполняете линейный поиск f<i>
, что приводит к чрезмерному количеству экземпляров.
Решение состоит в том, чтобы разрешить f<i>
вызов g<i,0>
. Это, в свою очередь, вызывает g<i,i/2>
и g<i/2,0>
, которые вызывают g<i,i/2 i/4>
, g<i/2 i/4,i/2>
, g<i/2,i/4>
и g<i/4, 0>
бесконечность. Вы, конечно, будете специализироваться g<__LINE__, __LINE__>
внутри REGISTER()
.
Создание экземпляра f<65536>
по-прежнему приведет к созданию 65536 экземпляров шаблона (фактически вы проверяете все предыдущие 65536 строк), но глубина рекурсии ограничена логарифмом (65536), или 16 уровнями. Это выполнимо.
Ответ №2:
Возможно, что-то вроде:
template<typename T>
struct register_struct {
virtual void operator()() = 0;
register_struct();
register_struct *pNext;
};
template<typename T>
struct registry {
static register_struct<T> *chain;
static void walk() {
register_struct<T> *p = chain;
while (p) {
(*p)();
p = p->pNext;
}
}
};
template<typename T>
register_struct<T> *registry<T>::chain = NULL;
template<typename T>
register_struct<T>::register_struct()
{
pNext = registry<T>::chain;
registry<T>::chain = this;
}
#define DECL_REGISTRY(name)
struct tag_##name { } ;
void name() { registry<tag_##name>::walk(); }
#define JOIN_EXPAND(x, y) JOIN_EXPAND_2(x, y)
#define JOIN_EXPAND_2(x, y) x ## y
// Invoke REGISTER_PRINT at file scope!
#define REGISTER_PRINT(name, text)
namespace {
static struct : public register_struct<tag_##name> {
void operator()() {
std::cout << text << std::endl;
}
} JOIN_EXPAND(rs_##name##_, __LINE__);
}
DECL_REGISTRY(foo);
REGISTER_PRINT(foo, "hello")
REGISTER_PRINT(foo, "world")
int main() {
foo();
return 0;
}
Это устранит проблемы с рекурсивным созданием экземпляра, но вы по-прежнему ограничены одной регистрацией в строке. Однако, поскольку регистрации находятся в области действия файла, это должно (надеюсь!) быть меньшей проблемой.
Обратите внимание, что это небезопасно использовать, если вы ожидаете вызвать их до main()
— вы должны разрешить статическим конструкторам завершить работу перед его использованием.
Комментарии:
1. Как я уже сказал, мне не нужны статические объекты.
Ответ №3:
Я когда-то делал нечто подобное, которое создавало экземпляры только ограниченного количества специализаций. Целью было объединить все специализации в массив указателей и получить к ним доступ с помощью одного метода через enum, но вы легко можете адаптировать его к вашим аналогичным (как я полагаю) потребностям.
#include <iostream>
using namespace std;
class I {
public:
virtual ~I() {};
virtual void P(int index) = 0;
enum Item {
Item0,
Item1,
Item2,
Item3,
Item4,
ItemNum
};
};
template <class T> class A: public I {
public:
A() {
Unroll<A<T>, ItemNum> tmp (m_F);
}
virtual ~A() {
}
void P(int index) { (this->*m_F[index])(); }
protected:
typedef void (A<T>::*F)();
F m_F[ItemNum];
template <int N> void p() { cout << "default!" << endl; }
template <class W, int C> struct Unroll
{
Unroll(typename W::F * dest)
{
dest[C-1] = amp; W::template p<C-1>;
Unroll<W, C-1> u(dest);
}
};
};
template <class T> template <class W> struct A<T>::Unroll<W, 0>
{ public: Unroll(typename W::F * dest) {} };
class B: public A<B>
{
public:
};
template <> template <> void A<B>::p<A<B>::Item1>() { cout << 1 << endl; }
template <> template <> void A<B>::p<A<B>::Item2>() { cout << 2 << endl; }
template <> template <> void A<B>::p<A<B>::Item4>() { cout << "it hacking works!" << endl; }
int main()
{
I *a = new B;
for (int i = 0; i < I::ItemNum; i) a->P(i);
return 0;
}
Ответ №4:
Вот решение, которое ограничивает рекурсию количеством фактически зарегистрированных функций. Вместо использования __LINE__
в качестве идентификатора я использовал BOOST_PP_COUNTER
, который представляет собой увеличивающийся счетчик, доступный препроцессору.
Хитрость в том, что вы не можете увеличить счетчик внутри макроса, поскольку увеличение выполняется путем включения файла заголовка. Следовательно, мне также пришлось полагаться на включение файла, поэтому для регистрации сообщения потребовалось две строки вместо одной (одна строка для определения сообщения и одна строка для его фактической регистрации).
Вот код:
//register.hpp
#include <boost/preprocessor/slot/counter.hpp>
// general template function, not defined
template <unsigned int ID>
void f();
// base case, to stop recursion
template <>
void f<0>() {}
// macro to "hide" the name of the header to include (which should be in a
// "hidden" folder like "detail" in Boost
#define REGISTER() "actually_register_msg.hpp"
//actually_register_msg.hpp
#include <boost/preprocessor/stringize.hpp>
// increment the counter
#include BOOST_PP_UPDATE_COUNTER()
template<>
inline void f< BOOST_PP_COUNTER >()
{
std::cout << BOOST_PP_STRINGIZE( MSG_TO_REGISTER ) << std::endl;
f< BOOST_PP_COUNTER - 1 >(); // call previously registered function
}
// to avoid warning and registering multiple times the same message
#undef MSG_TO_REGISTER
// id of the last registered function
#define LAST_FUNCTION_ID BOOST_PP_COUNTER
// main.cpp
#define MSG_TO_REGISTER message 1
#include REGISTER()
#define MSG_TO_REGISTER message 2
#include REGISTER()
#define MSG_TO_REGISTER message 3
#include REGISTER()
int main()
{
f< LAST_FUNCTION_ID >();
}
Как и ваш код, это выводит
message 3
message 2
message 1
Конечно, вызов registering немного менее привлекателен (один #define
и one #include
вместо одного вызова макроса), но вы избегаете большого количества ненужных экземпляров шаблона.