Пересмотр счетчиков времени компиляции C++

TL;DR

Прежде чем вы попытаетесь прочитать весь этот пост, знайте, что:

  1. решение представленной проблемы было найдено мною, но мне все еще не терпится узнать, верен ли анализ;
  2. Я упаковал решение в класс fameta::counter, который решает несколько оставшихся причуд. Вы можете найти его на github;
  3. вы можете увидеть это на работе над 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++, похоже, заключается в том, что после того, как компилятор оценивает параметры по умолчанию шаблонов функций при первом вызове функций, любой последующий вызов к этим функциям не вызывает повторную оценку параметров по умолчанию, поэтому никогда не создаются новые функции, но всегда ссылаются на ранее созданные.


Первые вопросы

  1. Вы действительно согласны с этим моим диагнозом?
  2. Если да, то является ли это новое поведение предписанным стандартом? Был ли предыдущий баг?
  3. Если нет, то в чем проблема?

Имея в виду вышеизложенное, я придумал обходной путь: пометить каждый вызов 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++ считает, что определил нечто, что, по его мнению, не было определено

Итак, получается, что clang++ считает, что определил что-то, что, по его мнению, он не определил, от чего у вас кружится голова, не так ли?


Вторая порция вопросов

  1. Является ли мой обходной путь допустимым для C++, или мне удалось обнаружить еще одну ошибку g++?
  2. Если это законно, обнаружил ли я какие-то неприятные ошибки clang++?
  3. Или я просто погрузился в темный подземный мир Undefined Behavior, так что виноват только я один?

В любом случае, я был бы рад приветствовать любого, кто хотел бы помочь мне выбраться из этой кроличьей норы, раздавая при необходимости головокружительные объяснения. :D


person Fabio A.    schedule 05.02.2020    source источник
comment
Связано: stackoverflow.com/questions/51601439/   -  person HolyBlackCat    schedule 05.02.2020
comment
Насколько я помню, у членов комитета по стандартизации есть четкое намерение запретить конструкции времени компиляции любого вида, формы или формы, которые не дают точно такой же результат каждый раз, когда они (гипотетически) оцениваются. Таким образом, это может быть ошибка компилятора, это может быть неправильно сформированный случай, не требующий диагностики, или это может быть что-то, что пропущено стандартом. Тем не менее, это противоречит духу стандарта. Мне жаль. Мне бы тоже понравились счетчики времени компиляции.   -  person bolov    schedule 05.02.2020
comment
@HolyBlackCat Должен признаться, мне очень трудно понять этот код. Похоже, что это может избежать необходимости явно передавать монотонно возрастающее число в качестве параметра функции next(), однако я не могу понять, как это работает. В любом случае, я нашел ответ на свою проблему здесь: stackoverflow.com/a/60096865/566849   -  person Fabio A.    schedule 06.02.2020
comment
@ФабиоА. Я тоже не совсем понимаю этот ответ. С тех пор, как я задал этот вопрос, я понял, что больше не хочу касаться счетчиков constexpr.   -  person HolyBlackCat    schedule 06.02.2020
comment
Хотя это небольшой забавный мысленный эксперимент, тот, кто действительно использовал этот код, должен был ожидать, что он не будет работать в будущих версиях C++, верно? В этом смысле результат определяет себя как ошибку.   -  person Aziuth    schedule 07.02.2020
comment
@Aziuth Я не совсем уверен в логическом скачке, который ты там делаешь. В настоящее время он соответствует стандарту, поэтому это не ошибка. Если позже стандарт изменится, нарушив совместимость с предыдущим кодом, это преднамеренное решение властных лиц. Мы, безусловно, можем попытаться заставить их передумать.   -  person Fabio A.    schedule 07.02.2020
comment
@ФабиоА. Ошибка - это в основном все нежелательное. Серьезный недостаток переносимости на более высокую версию — один из них. Я имею в виду, хорошо, можно принять решение, но у этого, кажется, есть очень хороший шанс прийти и укусить вас в будущем. Тем не менее, я бы сказал, что принимаю это, если кто-то может привести действительно хороший аргумент, почему это необходимо. Чего бы я даже и требовал только из-за отсутствия читабельности в этом одном.   -  person Aziuth    schedule 07.02.2020
comment
@Aziuth само существование предопределенного макроса __COUNTER__ во всех основных браузерах намекает на тот факт, что форма счетчика времени компиляции a, безусловно, требуется для достаточно большой базы кода. Однако этого недостаточно для всех желаемых вариантов использования, поскольку ему не хватает информации о контексте. Такие проекты, как copperspice, должны были изобрести свой собственный способ создания такого счетчика, который, однако, имеет ограниченные возможности и функциональность. Наличие универсального механизма для реализации контекстно-зависимых счетчиков времени компиляции открывает множество других вариантов использования. Один из них я упомянул в этом самом QA.   -  person Fabio A.    schedule 07.02.2020
comment
@ФабиоА. Дело в том, что не каждая проблема имеет решение. Вы говорите, что счетчики обязательны, ладно, тут не соглашусь. Возможно, это ошибка C++, не имеющая такой вещи. Уловки, вводящие счетчик, против которых активно возражают люди, решающие, как развивается C++, все еще не являются стабильной функцией. Я имею в виду, я понимаю, что вам может понадобиться такой код, но это обходной путь и, как таковая, ошибка. Опять же, я не говорю, что решение без ошибок, которым все довольны, действительно существует.   -  person Aziuth    schedule 07.02.2020
comment
@Aziuth, решение, которое я предоставил, использует стандартный C++ и работает во всех основных компиляторах. Несмотря на то, что CWG это не понравилось, очень может быть, что они не смогут сделать это незаконным, не нарушая другие части языка, на которые опирается другое программное обеспечение, даже не желая создавать противодействие.   -  person Fabio A.    schedule 07.02.2020
comment
@ Фабио А. У нас есть __FUNC__, который также генерирует разные вещи. Но у меня вопрос.. что если нам нужно несколько счетчиков в одном модуле? И этот счетчик ограничен модулем компиляции, поэтому, если код, использующий его, разделен, приращение счетчика нарушается? Возможно, кому-то не понравилась возможность подобных эффектов. Или, может быть, это связано с созданием экземпляров статических членов шаблонов.   -  person Swift - Friday Pie    schedule 07.02.2020
comment
@Swift-FridayPie Описанный мной подход позволяет использовать столько счетчиков, сколько необходимо, в одной и той же единице компиляции, если это то, что вы имеете в виду под модулем. Это счетчик времени компиляции, поэтому он не передает состояние между единицами компиляции. В этом смысле можно сказать, что он ломается, но, боюсь, с этим мало что можно сделать.   -  person Fabio A.    schedule 08.02.2020
comment
@FabioA Да, я имел в виду модуль, модуль - это вражеское слово переводчика. Последовательность компиляции в стандарте говорит о том, что реализациям разрешено сохранять информацию об используемых экземплярах в двоичных файлах. Таким образом, некоторые компиляторы поддерживают шаблонные методы, связанные с другими модулями (не из заголовков). Я боюсь, что такая реализация (по крайней мере, более старый Visual C++) может иметь блоки, мешающие друг другу.   -  person Swift - Friday Pie    schedule 08.02.2020
comment
Просто для придирки: Since Filip Roséen discovered/invented, in 2015 неверно, эта техника не была обнаружена и изобретена этим человеком в 2015 году, поскольку я использовал ее в 2011 году и явно не был ее изобретателем. Я думаю, вы можете отследить счетчики времени компиляции до 2005 года, вероятно...   -  person Synxis    schedule 04.02.2021
comment
Ваше предложение касается счетчиков времени компиляции, независимо от того, используются ли функции препроцессора или функции contexpr; счетчики времени компиляции явно не новы (посмотрите на это: stackoverflow.com/questions/6166337/ в 2011 году...). Я не уверен, что Розен был первым, кто изобрел счетчик времени компиляции, связанный с contexpr, поэтому утверждать, что это будет правильно, только если вы можете это доказать. Кроме того, Розен, возможно, первая, не интересна для вашего вопроса...   -  person Synxis    schedule 05.02.2021
comment
@Synxis Я отредактирую свой пост, чтобы было ясно, что я явно имел в виду технику инъекции друзей. Я думал, что это было ясно из остальной части поста. Спасибо за ваше замечание.   -  person Fabio A.    schedule 06.02.2021


Ответы (1)


После дальнейшего изучения выяснилось, что существует небольшая модификация, которую можно выполнить для функции next(), которая заставляет код правильно работать в версиях clang++ выше 7.0.0, но перестает работать во всех других версиях clang++.

Взгляните на следующий код, взятый из моего предыдущего решения.

template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N>())+1>::value) {
    return R;
}

Если вы обратите на него внимание, то он буквально пытается прочитать значение, связанное с slot<N>, добавить к нему 1, а затем связать это новое значение с тот же самый slot<N>.

Когда slot<N> не имеет связанного значения, вместо этого извлекается значение, связанное с slot<Y>, причем Y является самым высоким индексом, меньшим, чем N, так что slot<Y> имеет связанное значение.

Проблема с приведенным выше кодом заключается в том, что, несмотря на то, что он работает на g++, clang++ (правильно, я бы сказал?) заставляет reader(0, slot<N>()) постоянно возвращать то, что возвращалось, когда slot<N> не имел связанного значения. В свою очередь, это означает, что все слоты эффективно связаны с базовым значением 0.

Решение состоит в том, чтобы преобразовать приведенный выше код в этот:

template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N-1>())+1>::value) {
    return R;
}

Обратите внимание, что slot<N>() было изменено на slot<N-1>(). Это имеет смысл: если я хочу связать значение с slot<N>, это означает, что значение еще не связано, поэтому нет смысла пытаться его получить. Кроме того, мы хотим увеличить счетчик, и значение счетчика, связанного с slot<N>, должно быть равно единице плюс значение, связанное с slot<N-1>.

Эврика!

Однако это ломает версии clang++ ‹= 7.0.0.

Выводы

Мне кажется, что исходное решение, которое я опубликовал, имеет концептуальную ошибку, например:

  • В g++ есть причуда/ошибка/расслабление, которые компенсируются ошибкой моего решения и, тем не менее, в конечном итоге заставляют код работать.
  • Clang++ версии > 7.0.0 более строгие и не любят ошибку в исходном коде.
  • В версиях clang++ ‹= 7.0.0 есть ошибка, из-за которой исправленное решение не работает.

Суммируя все это, следующий код работает на всех версиях g++ и clang++.

#if !defined(__clang_major__) || __clang_major__ > 7
template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N-1>())+1>::value) {
    return R;
}
#else
template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N>())+1>::value) {
    return R;
}
#endif

Код как есть также работает с msvc. Компилятор icc не запускает 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, скриншот ниже.

введите здесь описание изображения

person Fabio A.    schedule 06.02.2020
comment
Я думаю, что этот ответ как бы закрывает тему, но я все же хотел бы знать, прав ли я в своем анализе, поэтому я подожду, прежде чем принять свой ответ как правильный, надеясь, что кто-то другой пройдет мимо и даст мне лучшую подсказку. или подтверждение. :) - person Fabio A.; 06.02.2020