TL; DR
Прежде чем пытаться прочитать весь этот пост, знайте, что:
- решение поставленной проблемы было найдено мной , но я все еще хочу знать, является ли анализ правильным;
- Я упаковал решение в
fameta::counter
класс, который решает несколько оставшихся уловок. Вы можете найти это на github ; - Вы можете видеть это на работе над Godbolt .
Как все начиналось
С тех пор как в 2015 году Филипп Розен обнаружил / изобрел черную магию, заключающуюся в том, что счетчики времени компилируются в C ++ , я был слегка одержим этим устройством, поэтому, когда CWG решила, что функциональность должна была уйти, я был разочарован, но все же надеялся, что их мнение можно изменить, показав им несколько убедительных вариантов использования.
Затем, пару лет назад, я решил еще раз взглянуть на эту штуку, чтобы uberswitch es можно было вкладывать - интересный вариант использования, на мой взгляд, - только чтобы обнаружить, что он больше не будет работать с новыми версиями доступные компиляторы, хотя выпуск 2118 находился (и остается ) в открытом состоянии: код компилируется, но счетчик не увеличивается.
О проблеме сообщалось на веб-сайте Розена, а в последнее время также и о стековом потоке : поддерживает ли C ++ счетчики времени компиляции?
Несколько дней назад я решил попытаться решить проблемы снова
Я хотел понять, что изменилось в компиляторах, из-за которых, казалось бы, действующий 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");
}
В компиляторах недавнего next()
выхода g ++ и clang ++ всегда возвращается 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, как и ожидалось, но с версией clang ++ выше 7.0.0 этого не происходит .
Чтобы добавить оскорбление к травме, мне фактически удалось сделать аварийное завершение работы clang ++ до версии 7.0.0, просто добавив параметр «context» в микс, так что счетчик фактически связан с этим контекстом и, таким образом, может перезапускаться каждый раз, когда определяется новый контекст, который открывает возможность использовать потенциально бесконечное количество счетчиков. В этом варианте clang ++ выше версии 7.0.0 не дает сбоя, но все равно не дает ожидаемого результата. Жить на кресте .
Потеряв любую подсказку о том, что происходит, я обнаружил веб-сайт 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;
Вы можете увидеть все это на Godbolt . Снимок экрана ниже.
Итак, кажется, что Clang ++ считает, что он определил нечто, что, как он полагает, он не определил , что заставляет вашу голову вращаться, не так ли?
Вторая группа вопросов
- Является ли мой обходной путь законным C ++ или мне удалось обнаружить еще одну ошибку g ++?
- Если это законно, обнаружил ли я какие-то неприятные ошибки в clang ++?
- Или я просто погрузился в темный подземный мир неопределенного поведения, так что я сам виноват?
В любом случае, я тепло приветствую любого, кто хочет помочь мне выбраться из этой кроличьей норы, предоставляя объяснения головной боли, если это будет необходимо. : D
next()
функции, однако я не могу понять, как это работает. В любом случае, я придумала ответ на свою проблему здесь: stackoverflow.com/a/60096865/566849