TL; DR
Перш ніж спробувати прочитати весь цей пост, знайте, що:
- рішення представленого питання я знайшов сам , але я все ще нетерплячий знати, чи правильний аналіз;
- Я розфасував це рішення в
fameta::counter
клас, який вирішує кілька залишків. Ви можете знайти його на github ; - ви можете побачити це на роботі на Godbolt .
Як все починалося
Оскільки Філіп Розен відкрив / винайшов у 2015 році чорну магію, яка збирає лічильники часу, у C ++ , я був м'яко одержимий цим пристроєм, тому коли CWG вирішив, що функціональність повинна працювати, я був розчарований, але все ще сподіваюся, що їх розум можна змінити, показавши їм кілька переконливих випадків використання.
Потім, пару років тому, я вирішив переглянути цю річ ще раз, щоб uberswitch es міг бути вкладений - цікавий випадок використання, на мій погляд, - лише щоб виявити, що він більше не працюватиме з новими версіями доступні компілятори, навіть якщо випуск 2118 був (і досі є ) у відкритому стані: код збирається, але лічильник не збільшуватиметься.
Про проблему повідомлялося на веб-сайті Roséen, а нещодавно також на stackoverflow: Чи підтримує C ++ лічильники часу компіляції?
Кілька днів тому я вирішив спробувати вирішити проблеми ще раз
Я хотів зрозуміти, що змінилося в компіляторах, які зробили, здавалося б, все ще дійсним C ++, більше не працювати. З цією метою я шукав широко і далеко в інтернеті, щоб хтось говорив про це, але безрезультатно. Тому я почав експериментувати і дійшов висновків, що я тут представляю, сподіваючись отримати зворотний зв’язок від більш знаних, ніж я тут.
Нижче я представляю оригінальний код Roséen для наочності. Для пояснення того, як це працює, зверніться до його веб-сайту :
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 ++ останніми компіляторами-ish 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 не завершується, але все ж не дає очікуваного результату. Живи на богоболі .
Втративши будь-яку підказку про те, що відбувається, я виявив веб-сайт 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 ++ взагалі, або ж мені вдалося тільки відкрити ще г ++ помилку?
- Якщо це законно, то чи виявив я якихось неприємних кланг ++ помилок?
- Або я просто заглибився у темний підземний світ Невизначеної поведінки, тож я сам винен?
У будь-якому випадку я б щиро вітав усіх, хто хотів допомогти мені вийти з цієї кролячої нори, видавши при необхідності пояснення з приводу головного болю. : D
next()
функції, проте я не можу реально зрозуміти, як це працює. У будь-якому випадку я знайшов відповідь на власну проблему тут: stackoverflow.com/a/60096865/566849