Использование аргумента функции как части постоянного выражения - gcc vs clang

Рассмотрим следующий фрагмент кода:

template <bool> struct B { };

template <typename T>
constexpr bool pred(T t) { return true; } 

template <typename T>
auto f(T t) -> decltype(B<pred(t)>{})
{
}
  • clang++ (соединительная линия) компилирует код

  • g++ (trunk) завершает компиляцию со следующей ошибкой:

    src:7:34: error: template argument 1 is invalid
    auto f(T t) -> decltype(B<pred(t)>{})
                                    ^
    
    src:7:34: error: template argument 1 is invalid
    src:7:34: error: template argument 1 is invalid
    src:7:34: error: template argument 1 is invalid
    src:7:34: error: template argument 1 is invalid
    src:7:34: error: template argument 1 is invalid
    src:7:25: error: invalid template-id
    auto f(T t) -> decltype(B<pred(t)>{})
                            ^
    
    src:7:36: error: class template argument deduction failed:
    auto f(T t) -> decltype(B<pred(t)>{})
                                        ^
    
    src:7:36: error: no matching function for call to 'B()'
    src:1:24: note: candidate: 'template<bool <anonymous> > B()-> B<<anonymous> >'
    template <bool> struct B { };
                            ^
    
    src:1:24: note:   template argument deduction/substitution failed:
    src:7:36: note:   couldn't deduce template parameter '<anonymous>'
    auto f(T t) -> decltype(B<pred(t)>{})
                                        ^
    

    живой пример на godbolt.org


Хотя диагностика g++ вводит в заблуждение, я предполагаю, что проблема здесь в том, что t не является постоянным выражением. Изменение кода на...

decltype(B<pred(T{})>{})

... исправляет ошибку компиляции на g++: живой пример на godbolt.org


Какой компилятор работает здесь правильно?

Ответ 1

GCC ошибочен. Нет правила, которое предотвращает использование параметров функции в постоянном выражении таким образом.

Однако вы не можете использовать значение параметра в таком контексте, а набор типов T для которых f является вызываемым, довольно ограничен. Чтобы понять, почему, мы должны рассмотреть, какие конструкции будут оцениваться при оценке выражения pred(t):

// parameters renamed for clarity
template <typename U>
constexpr bool pred(U u) { return true; } 

template <typename T>
auto f(T t) -> decltype(B<pred(t)>{});

Семантика оценки для вызова pred(t) выглядит следующим образом:

  1. copy-initialize pred параметр u из f параметра t
  2. оценивать тело pred, которое тривиально создает значение bool true
  3. уничтожить u

Таким образом, f допускается только для типов T для которых приведенное выше включает только конструкции, которые действительны при постоянной оценке (см. [Expr.const] p2 для правил). Требования:

  • T должен быть буквальным типом
  • копирование-инициализация u из t должно быть постоянным выражением и, в частности, не должно выполнять преобразование lvalue-to-r на любом члене t (потому что их значения неизвестны) и не должны указывать какой-либо ссылочный элемент t

На практике это означает, что f является вызываемым, если T является пустым типом класса с конструктором копии по умолчанию или если T является типом класса, конструктор которого является constexpr и не читает никаких членов его аргумента, или (как ни странно), если T is std::nullptr_t (хотя clang в настоящее время получает случай nullptr_t неправильно).

Ответ 2

Компилятор ожидает параметр в этом контексте, потому что ему необходимо оценить полный (шаблонный) тип функции. Учитывая реализацию pred, любое значение будет работать в этом месте. Здесь он привязывает тип шаблона параметра f к аргументу. Компилятор g++, по-видимому, делает упрощающее предположение о том, что функция шаблона constexpr каким-то образом будет изменена любыми параметрами, если они не являются также const, которые, как вы показали, и clang согласны, не обязательно так.

Все сводится к тому, насколько глубоко внутри реализации функции компилятор идет, чтобы отметить функцию как неконстантную из-за неконстантного вклада в возвращаемое значение.

Затем возникает вопрос о том, была ли эта функция создана и требует, чтобы компилятор фактически скомпилировал код и выполнил синтаксический анализ шаблона, который, по крайней мере, с g++, представляется другим уровнем компиляции.

Затем я пошел на стандарт, и они любезно позволяют автору компилятора сделать именно это упрощающее предположение, а создание экземпляра шаблона должно работать только для f<const T> или f <const T&>

constexpr 'должны иметь: каждый из его параметров должен быть LiteralType

Таким образом, код шаблона должен компилироваться, но сбой, если он создается с помощью неконстантного T.

Ответ 3

t не является значением constexpr, это значение pred(t) не является constexpr. Вы не можете использовать его в B<pred(t)> потому что для этого требуется constexpr.

Эта версия правильно компилируется:

template <bool> struct B { };

template <typename T>
constexpr bool pred(T t) { return true; } 

template <typename T, T t>
auto f() -> decltype(B<pred(t)>{})
{
}

https://godbolt.org/g/ydbj1X

Другой действующий код:

template <typename T>
auto f(T t) -> decltype(pred(t))
{
}

Это связано с тем, что вы не оцениваете pred(t) только вы получаете информацию о типе. B<pred(t)> нужна оценка pred(t) иначе вы получите B<true> или B<false>, для любого нормального значения вы не сможете этого сделать.

std::integral_constant<int, 0>{} может работать в случае Клана, вероятно, потому, что его значение встраивается как часть типа и всегда одинаково. Если мы немного изменим код:

template <typename T>
auto f(T t) -> decltype(B<pred(decltype(t){})>{})
{
    return {};
}

и Clang, и GCC компилируют его, в случае std::integral_constant decltype(t){}, и t и decltype(t){} всегда имеют одинаковое значение.