C ++ скласти лічильники часу, переглянутий


28

TL; DR

Перш ніж спробувати прочитати весь цей пост, знайте, що:

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


Перші запитання

  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 не завершується, але все ж не дає очікуваного результату. Живи на богоболі .

Втративши будь-яку підказку про те, що відбувається, я виявив веб-сайт 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 ++ вважає, що він визначив щось, що він вважає, що не визначив

Отже, схоже, що clang ++ вважає, що він визначив щось, що, на його думку, не визначило , що змушує твою голову крутитися, чи не так?


Друга партія питань

  1. Це мій обхідний правової C ++ взагалі, або ж мені вдалося тільки відкрити ще г ++ помилку?
  2. Якщо це законно, то чи виявив я якихось неприємних кланг ++ помилок?
  3. Або я просто заглибився у темний підземний світ Невизначеної поведінки, тож я сам винен?

У будь-якому випадку я б щиро вітав усіх, хто хотів допомогти мені вийти з цієї кролячої нори, видавши при необхідності пояснення з приводу головного болю. : D



2
Як я пам’ятаю, у стандартних комітетів люди мають чіткий намір заборонити конструкції, складені за часом, будь-якого виду, форми чи форми, які не дають абсолютно однакового результату щоразу, коли вони (гіпотетично) оцінюються. Таким чином, це може бути помилка компілятора, це може бути "неправильно сформована, не потрібна діагностика", або це може бути щось, що стандарт пропустив. Тим не менш, це суперечить "духу стандарту". Вибачте. Я також хотів би скласти лічильники часу.
болов

@HolyBlackCat Я мушу визнати, що мені дуже важко обійти цей код. Схоже, це могло б уникнути необхідності явно передавати монотонно зростаюче число як параметр next()функції, проте я не можу реально зрозуміти, як це працює. У будь-якому випадку я знайшов відповідь на власну проблему тут: stackoverflow.com/a/60096865/566849
Фабіо А.

@FabioA. Я теж не зовсім розумію цю відповідь. Починаючи задавати це питання, я зрозумів, що більше ніколи не хочу торкатися лічильників конспектів.
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
Я думаю, що така відповідь закриває тему, але я все одно хотів би знати, чи я правий у своєму аналізі, тому я зачекаю, перш ніж приймати власну відповідь як правильну, сподіваючись, що хтось інший пройде повз і дасть мені кращу підказку або підтвердження. :)
Фабіо А.
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.