TL;DR
Прежде чем вы попытаетесь прочитать весь этот пост, знайте, что:
- решение представленной проблемы было найдено мною, но мне все еще не терпится узнать, верен ли анализ;
- Я упаковал решение в класс
fameta::counter
, который решает несколько оставшихся причуд. Вы можете найти его на github; - вы можете увидеть это на работе над godbolt.
Как все началось
С тех пор, как в 2015 году Филип Розен обнаружил/изобрел черную магию, заключающуюся в том, что счетчики времени компиляции с помощью внедрения друзей в C++, я был немного одержим этим устройством, поэтому, когда CWG решил, что функциональность должна быть удалена Я был разочарован, но все же надеялся, что их мнение можно изменить, показав им несколько убедительных вариантов использования.
Затем, пару лет назад, я решил еще раз взглянуть на эту штуку, чтобы uberswitches могут быть вложены друг в друга — на мой взгляд, интересный вариант использования — только для того, чтобы обнаружить, что это больше не будет работать с новыми версиями доступные компиляторы, хотя выпуск 2118 был (и до сих пор) в открытом состоянии: код скомпилируется, но счетчик не увеличится.
О проблеме сообщалось на веб-сайте Розен, а недавно также на stackoverflow. : Поддерживает ли 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");
}
Как для g++, так и для clang++ компиляторов последних версий 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, как и ожидалось, но с clang++ версии выше 7.0.0 это не работает! т.
Чтобы добавить оскорбление к травме, мне действительно удалось вызвать сбой clang++ до версии 7.0.0, просто добавив параметр контекста в микс, так что счетчик фактически привязан к этому контексту и, как таковой, может быть перезапущен каждый раз, когда определяется новый контекст, который открывает возможность использовать потенциально бесконечное количество счетчиков. С этим вариантом clang++ выше версии 7.0.0 не падает, но все равно не дает ожидаемого результата. Жить на godbolt.
Потеряв всякое представление о том, что происходит, я обнаружил веб-сайт 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++?
- Или я просто погрузился в темный подземный мир Undefined Behavior, так что виноват только я один?
В любом случае, я был бы рад приветствовать любого, кто хотел бы помочь мне выбраться из этой кроличьей норы, раздавая при необходимости головокружительные объяснения. :D
next()
, однако я не могу понять, как это работает. В любом случае, я нашел ответ на свою проблему здесь: stackoverflow.com/a/60096865/566849 а> - person Fabio A.   schedule 06.02.2020__COUNTER__
во всех основных браузерах намекает на тот факт, что форма счетчика времени компиляции a, безусловно, требуется для достаточно большой базы кода. Однако этого недостаточно для всех желаемых вариантов использования, поскольку ему не хватает информации о контексте. Такие проекты, какcopperspice
, должны были изобрести свой собственный способ создания такого счетчика, который, однако, имеет ограниченные возможности и функциональность. Наличие универсального механизма для реализации контекстно-зависимых счетчиков времени компиляции открывает множество других вариантов использования. Один из них я упомянул в этом самом QA. - person Fabio A.   schedule 07.02.2020__FUNC__
, который также генерирует разные вещи. Но у меня вопрос.. что если нам нужно несколько счетчиков в одном модуле? И этот счетчик ограничен модулем компиляции, поэтому, если код, использующий его, разделен, приращение счетчика нарушается? Возможно, кому-то не понравилась возможность подобных эффектов. Или, может быть, это связано с созданием экземпляров статических членов шаблонов. - person Swift - Friday Pie   schedule 07.02.2020Since Filip Roséen discovered/invented, in 2015
неверно, эта техника не была обнаружена и изобретена этим человеком в 2015 году, поскольку я использовал ее в 2011 году и явно не был ее изобретателем. Я думаю, вы можете отследить счетчики времени компиляции до 2005 года, вероятно... - person Synxis   schedule 04.02.2021