Счетчики времени компиляции C ++, вновь

28

TL; DR

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

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


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

  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, просто добавив параметр «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 ++ считает, что он определил то, что он не определил

Итак, кажется, что Clang ++ считает, что он определил нечто, что, как он полагает, он не определил , что заставляет вашу голову вращаться, не так ли?


Вторая группа вопросов

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

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

Фабио А.
источник
2
Связанный: stackoverflow.com/questions/51601439/…
HolyBlackCat
2
Насколько я помню, у членов стандартного комитета есть четкое намерение запретить конструкции времени компиляции любого вида, формы или формы, которые не дают тот же самый результат каждый раз, когда они (гипотетически) оцениваются. Таким образом, это может быть ошибка компилятора, это может быть «неправильно сформированный, диагноз не требуется», или это может быть что-то, что стандарт пропустил. Тем не менее это идет вразрез с «духом стандарта». Я прошу прощения. Мне бы тоже понравились счетчики времени компиляции.
Болов
@HolyBlackCat Должен признаться, мне очень трудно разобраться в этом коде. Похоже, что это может избежать необходимости явно передавать монотонно увеличивающееся число в качестве параметра next()функции, однако я не могу понять, как это работает. В любом случае, я придумала ответ на свою проблему здесь: stackoverflow.com/a/60096865/566849
Фабио А.
@FabioA. Я тоже не совсем понимаю этот ответ. Задав этот вопрос, я понял, что больше не хочу касаться счетчиков constexpr.
HolyBlackCat
Хотя это небольшой забавный мысленный эксперимент, кому-то, кто на самом деле использовал этот код, в значительной степени придется ожидать, что он не будет работать в будущих версиях C ++, верно? В этом смысле результат определяет себя как ошибку.
Азиут

Ответы:

5

После дальнейшего изучения выясняется, что существует незначительная модификация, которая может быть выполнена для 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. МАЯ компилятор не вызывает 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 ниже.

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

Фабио А.
источник
2
Я думаю, что этот ответ как бы закрывает тему, но я все же хотел бы знать, прав ли я в своем анализе, поэтому я подожду, прежде чем принять свой собственный ответ как правильный, надеясь, что кто-то другой пройдет мимо и даст мне лучшую подсказку или подтверждение. :)
Фабио А.