TL; DR
Прежде чем пытаться прочитать весь этот пост, знайте, что:
- решение представленной проблемы было найдено мной лично, но я все еще хочу знать, верен ли анализ;
- Я упаковал решение в класс
fameta::counter
, который решает несколько оставшихся ошибок. Вы можете найти его на github ; - вы можете увидеть его на работа над Godbolt .
Как все это началось
С тех пор, как Филипп Розен обнаружил / изобрел в 2015 году черные маги c, которые счетчики времени компиляции находятся в C ++ , я был слегка одержим этим устройством, поэтому, когда CWG решил эта функциональность должна была go Я был разочарован, но все еще надеялся, что их ум можно изменить, показав им несколько убедительных вариантов использования.
Затем, через пару лет go я решил еще раз взглянем на это, так что uberswitch es может быть вложен - на мой взгляд, интересный вариант использования - только для того, чтобы обнаружить, что он больше не будет работать с новыми версиями доступных компиляторов, даже если выпуск 2118 был (и все еще ) в открытом состоянии: код скомпилируется, но счетчик не увеличится.
О проблеме было сообщено на веб-сайте Розена и недавно также в стеке: Поддерживает ли C ++ счетчики времени компиляции?
Несколько дней go Я решил снова попытаться решить эти проблемы.
Я хотел понять, что изменилось в компиляторах, из-за которых, по-видимому, все еще действующий C ++ больше не работал. С этой целью я искал широкую и дальнюю сеть, чтобы кто-то говорил об этом, но безрезультатно. Итак, я начал экспериментировать и пришел к некоторым выводам, которые я представляю здесь, в надежде получить обратную связь от более осведомленных людей, чем здесь.
Ниже я представляю оригинальный код Розена для Ради ясности. Для объяснения того, как это работает, см. Его веб-сайт :
template<int N>
struct flag {
friend constexpr int adl_flag (flag<N>);
};
template<int N>
struct writer {
friend constexpr int adl_flag (flag<N>) {
return N;
}
static constexpr int value = N;
};
template<int N, int = adl_flag (flag<N> {})>
int constexpr reader (int, flag<N>) {
return N;
}
template<int N>
int constexpr reader (float, flag<N>, int R = reader (0, flag<N-1> {})) {
return R;
}
int constexpr reader (float, flag<0>) {
return 0;
}
template<int N = 1>
int constexpr next (int R = writer<reader (0, flag<32> {}) + N>::value) {
return R;
}
int main () {
constexpr int a = next ();
constexpr int b = next ();
constexpr int c = next ();
static_assert (a == 1 && b == a+1 && c == b+1, "try again");
}
С компиляторами g ++ и clang ++ latest-i sh, next()
всегда возвращает 1. Немного поэкспериментировав, проблема, по крайней мере с g ++, заключается в том, что, как только компилятор оценивает параметры по умолчанию для шаблонов функций при первом вызове функций, любой последующий вызов этих функций не вызывает переоценку параметров по умолчанию, таким образом, никогда не создавая новые функции, а всегда ссылаясь на ранее созданные.
Первые вопросы
- Вы действительно согласны с этим моим диагнозом?
- Если да, предписано ли это новое поведение стандартом? Был ли предыдущий баг?
- Если нет, то в чем проблема?
Учитывая вышесказанное, я придумал обходной путь: отметьте каждый next()
вызов с монотонно увеличивающимся уникальным идентификатором для передачи вызываемым абонентам, чтобы ни один вызов не был таким же, что вынуждает компилятор каждый раз переоценивать все аргументы.
Это кажется обузой чтобы сделать это, но, думая об этом, можно просто использовать стандартные __LINE__
или __COUNTER__
-подобные (где это возможно) макросы, скрытые в counter_next()
-подобном макросе.
Итак, я подошел со следующим, который я представляю в наиболее упрощенной форме, которая показывает проблему, о которой я буду говорить позже.
template <int N>
struct slot;
template <int N>
struct slot {
friend constexpr auto counter(slot<N>);
};
template <>
struct slot<0> {
friend constexpr auto counter(slot<0>) {
return 0;
}
};
template <int N, int I>
struct writer {
friend constexpr auto counter(slot<N>) {
return I;
}
static constexpr int value = I-1;
};
template <int N, typename = decltype(counter(slot<N>()))>
constexpr int reader(int, slot<N>, int R = counter(slot<N>())) {
return R;
};
template <int N>
constexpr int reader(float, slot<N>, int R = reader(0, slot<N-1>())) {
return R;
};
template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N>())+1>::value) {
return R;
}
int a = next<11>();
int b = next<34>();
int c = next<57>();
int d = next<80>();
Вы можете наблюдать результаты выше на godbolt , который я Скриншот для ленивых.
И, как вы можете видеть, с транком g ++ и clang ++ до 7.0.0 работает! , счетчик увеличивается с 0 до 3, как и ожидалось, но с кланом Версия g ++ выше 7.0.0 это не .
Чтобы добавить оскорбление ране, мне фактически удалось сделать clang ++ до версии 7.0. 0 cra sh, просто добавив в контекст параметр «context», так что счетчик фактически связан с этим контекстом и, как таковой, может быть перезапущен в любое время, когда определен новый контекст, который открывается для возможности использовать потенциально бесконечное количество счетчиков. В этом варианте clang ++ выше версии 7.0.0 не обрабатывает sh, но все равно не дает ожидаемого результата. Живи на кресте .
Потеряв любую подсказку о том, что происходит, я обнаружил веб-сайт cppinsights.io , который позволяет увидеть, как и когда создаются шаблоны. Используя эту службу , я думаю, что clang ++ не фактически определяет какую-либо из friend constexpr auto counter(slot<N>)
функций всякий раз, когда создается экземпляр writer<N, I>
.
Попытка явно вызвать counter(slot<N>)
для любого данного N, который должен был быть уже создан, похоже, дает основание для этой гипотезы.
Однако, если я попытаюсь явно создать экземпляр writer<N, I>
для любого учитывая N
и I
, которые уже должны были быть созданы, затем clang ++ жалуется на переопределенный friend constexpr auto counter(slot<N>)
.
Чтобы проверить вышеизложенное, я добавил еще две строки в предыдущий исходный код.
int test1 = counter(slot<11>());
int test2 = writer<11,0>::value;
Вы можете все это увидеть сами на кресте . Снимок экрана ниже.
Итак, похоже, что clang ++ считает, что он определил то, что, как он считает, он не определил , что заставляет вашу голову кружиться, не так ли?
Вторая группа вопросов
- Является ли мой обходной путь законным C ++ вообще, или мне удалось просто обнаружить еще одну ошибку g ++?
- Если это законно, я поэтому обнаружил некоторые неприятные ошибки clang ++?
- Или я просто углубился в темный подземный мир Неопределенного поведения, поэтому я я сам виноват?
В любом случае, я бы тепло приветствовал любого, кто хотел помочь мне выбраться из этой кроличьей норы, предоставив объяснения от головной боли, если это будет необходимо. : D