Подтвердить что ты не робот

Почему удаление параметра по умолчанию прерывает этот счетчик constexpr?

Рассмотрим следующий код, который реализует счетчик времени компиляции.

#include <iostream>

template<int>
struct Flag { friend constexpr int flag(Flag); };

template<int N>
struct Writer
{
    friend constexpr int flag(Flag<N>) { return 0; }
};

template<int N>
constexpr int reader(float, Flag<N>) { return N; }

template<int N, int = flag(Flag<N>{})>
constexpr int reader(int, Flag<N>, int value = reader(0, Flag<N + 1>{}))
{
    return value;
}

template<int N = reader(0, Flag<0>{}), int = sizeof(Writer<N>) >
constexpr int next() { return N; }


int main() {
    constexpr int a = next();
    constexpr int b = next();
    constexpr int c = next();
    constexpr int d = next();
    std::cout << a << b << c << d << '\n'; // 0123
}

Для второй перегрузки reader, если я поместил параметр по умолчанию внутри тела функции, например:

template<int N, int = flag(Flag<N>{})>
constexpr int reader(int, Flag<N>)
{
    return reader(0, Flag<N + 1>{});
}

Тогда выход будет выглядеть следующим образом:

0111

Почему это происходит? Что делает вторую версию больше не работать?

Если это имеет значение, я использую Visual Studio 2015.2.

4b9b3361

Ответ 1

Без value передается как параметр, ничто не останавливает компилятор от кеширования на вызов reader(0, Flag<1>).

В обоих случаях первый вызов next() будет работать так, как ожидалось, так как он немедленно приведет к SFINAEing к reader(float, Flag<0>).

Второй next() будет оценивать reader<0,0>(int, ...), который зависит от reader<1>(float, ...), который может быть кэширован, если он не зависит от параметра value.

К сожалению (и по иронии судьбы) лучший источник, который я нашел, который подтверждает, что constexpr вызовы могут кэшироваться, это комментарий @MSalters к этому вопросу.

Чтобы проверить, что ваш конкретный компилятор кэширует /memoizes, подумайте о вызове

constexpr int next_c() { return next(); }

вместо next(). В моем случае (VS2017) выход переходит в 0000.

next() защищен от кеширования тем фактом, что его аргументы шаблона по умолчанию меняются с каждым экземпляром, поэтому каждый раз он выполняет отдельную функцию. next_c() не является шаблоном вообще, поэтому он может быть кэширован, а значит reader<1>(float, ...).

Я действительно считаю, что это не ошибка, и компилятор может законно ожидать, что constexpr в контексте компиляции будет чистыми функциями.

Вместо этого этот код следует считать плохо сформированным, и он скоро будет, как отмечали другие.

Ответ 2

Актуальность value заключается в том, что она участвует в разрешении перегрузки. В соответствии с правилами SFINAE ошибки создания шаблона без исключения исключают кандидатов из разрешения перегрузки. Но он создает экземпляр Flag<N+1>, что приводит к тому, что разрешение перегрузки станет жизнеспособным в следующий раз (!). Таким образом, вы считаете успешные экземпляры.

Почему ваша версия ведет себя по-другому? Вы по-прежнему ссылаетесь на Flag<N+1>, но в реализации этой функции. Это важно. При использовании шаблонов функций для SFINAE следует учитывать объявление , но затем создается только выбранная перегрузка. Ваше объявление просто template<int N, int = flag(Flag<N>{})> constexpr int reader(int, Flag<N>); и не зависит от Flag<N+1>.

Как отмечено в комментариях, не рассчитывайте на этот счетчик;)