Счетчики времени компиляции C ++, вновь - PullRequest
28 голосов
/ 05 февраля 2020

TL; DR

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

  1. решение представленной проблемы было найдено мной лично, но я все еще хочу знать, верен ли анализ;
  2. Я упаковал решение в класс fameta::counter, который решает несколько оставшихся ошибок. Вы можете найти его на github ;
  3. вы можете увидеть его на работа над Godbolt .

Как все это началось

С тех пор, как Филипп Розен обнаружил / изобрел в 2015 году черные маги c, которые счетчики времени компиляции находятся в C ++ , я был слегка одержим этим устройством, поэтому, когда CWG решил эта функциональность должна была go Я был разочарован, но все еще надеялся, что их ум можно изменить, показав им несколько убедительных вариантов использования.

Затем, через пару лет go я решил еще раз взглянем на это, так что uberswitch es может быть вложен - на мой взгляд, интересный вариант использования - только для того, чтобы обнаружить, что он больше не будет работать с новыми версиями доступных компиляторов, даже если выпуск 2118 был (и все еще ) в открытом состоянии: код скомпилируется, но счетчик не увеличится.

О проблеме было сообщено на веб-сайте Розена и недавно также в стеке: Поддерживает ли C ++ счетчики времени компиляции?

Несколько дней go Я решил снова попытаться решить эти проблемы.

Я хотел понять, что изменилось в компиляторах, из-за которых, по-видимому, все еще действующий 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 ++ latest-i sh, 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 , который я Скриншот для ленивых.

enter image description here

И, как вы можете видеть, с транком g ++ и clang ++ до 7.0.0 работает! , счетчик увеличивается с 0 до 3, как и ожидалось, но с кланом Версия g ++ выше 7.0.0 это не .

Чтобы добавить оскорбление ране, мне фактически удалось сделать clang ++ до версии 7.0. 0 cra sh, просто добавив в контекст параметр «context», так что счетчик фактически связан с этим контекстом и, как таковой, может быть перезапущен в любое время, когда определен новый контекст, который открывается для возможности использовать потенциально бесконечное количество счетчиков. В этом варианте clang ++ выше версии 7.0.0 не обрабатывает sh, но все равно не дает ожидаемого результата. Живи на кресте .

Потеряв любую подсказку о том, что происходит, я обнаружил веб-сайт 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++ believes it has defined something that it believes it hasn't defined

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


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

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

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

1 Ответ

5 голосов
/ 06 февраля 2020

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

Eureka!

Это нарушает версии clang ++ <= 7.0.0. </p>

Выводы

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

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

Подводя итог, приведенный ниже код работает на всех версиях 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

Код "как есть" также работает с msv c. Компилятор i cc не запускает 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 может прийти на помощь.

доказательство находится на кресте , снимок экрана ниже.

enter image description here

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...