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Ответы:
После дальнейшего изучения выясняется, что существует незначительная модификация, которая может быть выполнена для
next()
функции, которая заставляет код работать должным образом на версиях clang ++ выше 7.0.0, но заставляет его перестать работать для всех других версий clang ++.Посмотрите на следующий код, взятый из моего предыдущего решения.
Если вы обращаете на это внимание, то буквально он пытается прочитать значение, связанное с ним
slot<N>
, добавить 1 к нему и затем связать это новое значение с тем же самымslot<N>
.Когда
slot<N>
не имеет связанного значения,slot<Y>
вместо этого извлекается значение, связанное с , сY
наивысшим индексом, меньшим, чем у тогоN
, которыйslot<Y>
имеет ассоциированное значение.Проблема с приведенным выше кодом состоит в том, что, хотя он работает на g ++, clang ++ (справедливо, я бы сказал?) Заставляет
reader(0, slot<N>())
постоянно возвращать то, что он возвращал, когдаslot<N>
не имел ассоциированного значения. В свою очередь это означает, что все слоты эффективно связаны с базовым значением0
.Решение состоит в том, чтобы преобразовать вышеупомянутый код в этот:
Обратите внимание, что
slot<N>()
был изменен вslot<N-1>()
. Это имеет смысл: если я хочу связать значение сslot<N>
, это означает, что никакое значение еще не связано, поэтому нет смысла пытаться получить его. Кроме того, мы хотим увеличить счетчик, и значение счетчика, связанного сslot<N>
, должно быть равным единице плюс значение, связанное сslot<N-1>
.Эврика!
Это нарушает версии Clang ++ <= 7.0.0.
Выводы
Мне кажется, что исходное решение, которое я разместил, содержит концептуальную ошибку, такую что:
Подводя итог всему этому, следующий код работает на всех версиях g ++ и clang ++.
Код как есть и работает с msvc. МАЯ компилятор не вызывает SFINAE при использовании
decltype(counter(slot<N>()))
, предпочитая жаловаться не в состоянииdeduce the return type of function "counter(slot<N>)"
из - заit has not been defined
. Я считаю, что это ошибка , которую можно обойти, выполнив SFINAE по прямому результатуcounter(slot<N>)
. Это работает и на всех других компиляторах, но g ++ решает выпустить обильное количество очень раздражающих предупреждений, которые нельзя отключить. Так что и в этом случае#ifdef
мог прийти на помощь.Доказательство на godbolt , screnshotted ниже.
источник