Случается ли всегда конструирование NTTP для CTAD, даже если задано значение типа этой специализации?

Вопрос или проблема

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

https://godbolt.org/z/5To7nKEP3

template<int N>
struct Bar {
    int n = N;
    constexpr Bar() {}
    constexpr Bar(const Bar& b): n{b.n - 1} {}
};

template<Bar b> constexpr int get_n() { return b.n; }

constexpr auto b = Bar<1>{};
static_assert(b.n == 1);

#ifdef _MSC_VER
static_assert(get_n<b>() == 1);
#else
static_assert(get_n<b>() == 0);
#endif

Вызывает и выводит из конструктора(ов) Bar для определения реального параметра шаблона, даже если заданный аргумент является типом специализации Bar, что не согласуется с MSVC.

Что говорит стандарт?

Программа должна быть неправильной для любого из static_assert.

Во-первых, отметим, что это ничем не связано с CTAD. Если вы явно указываете Bar<1> в качестве типа параметра шаблона, поведение компилятора остается тем же.

Выявление аргументов шаблона классов всегда выполняется и будет выводить Bar<1> здесь.

Однако вопрос теперь в том, как объект параметра шаблона инициализируется из аргумента шаблона, чтобы определить соответствующую специализацию get_n, которая будет вызвана.

Как именно это должно происходить для типов классов, было недостаточно описано, см. CWG проблема 2459. Я думаю, именно поэтому вы видите расхождение компиляторов в вашем примере.

Решение проблемы с документом P2308R1 в конце 2023 года делает так, что вызов get_n<b>() будет неправильным.

В основном новые правила утверждают, применяя к вашему примеру, что мы начинаем с воображаемой переменной, определенной как

constexpr Bar<1> v = b;

И затем мы дальше воображаем, что объект параметра шаблона (также типа Bar<1>, как было выведено ранее) инициализируется копированием из выражения v. Если затем объект параметра шаблона не эквивалентен-аргументу-шаблона объекту v (т.е. они имеют “одно и то же” значение (и тип)), программа будет неправильной.

Это происходит в вашем случае, потому что конструктор копирования (который используется в инициализации v и в копийной инициализации объекта параметра шаблона) создаст другое значение в новом объекте, чем в исходном объекте.

Похоже, что ни один из компиляторов еще не реализовал это.

Ответ или решение

Вопрос о том, всегда ли происходит построение NTTP (non-type template parameter) для CTAD (class template argument deduction), даже если значение типа этой специализации указано, является тонким и требует внимательного анализа специфики работы компиляторов и стандартов C++.

Ваша программа должна быть недопустимой для обоих static_assert, предшествующих в коде. Важно отметить, что это не связано с CTAD. Если вы явно указываете Bar<1> в качестве параметра шаблона, поведение компилятора остается прежним.

В классах, использующих параметры шаблонов, компиляторы всегда выполняют вывод аргументов шаблона и в данном случае выведут Bar<1>. Однако вопрос касается того, как инициализируется объект параметра шаблона из указанного аргумента, чтобы определить соответствующую специализацию функции get_n, которая будет вызвана.

На самом деле, стандарты C++ не совсем чётко указывали, как именно это должно происходить для типов классов. Это было причиной появления расхождений между компиляторами, как вы наблюдаете между Clang, GCC и MSVC.

Недавнее решение проблемы, изложенное в документе P2308R1, обновляет правила так, что вызов get_n<b>() будет недопустимым. В соответствии с новыми правилами, можно представить переменную, определённую как:

constexpr Bar<1> v = b;

И затем дополнительно представить, что объект параметра шаблона (также типа Bar<1>, как было выведено ранее) инициализируется из выражения v. Если объект параметра шаблона не является template-argument-equivalent (т.е. они не имеют «одинакового» значения (и типа)), то программа считается недопустимой.

В вашем случае это происходит из-за того, что конструктор копирования (который используется для инициализации v и в копии объекта параметра шаблона) создаёт значение в новом объекте, отличное от значения исходного объекта.

На данный момент ни один из компиляторов не реализовал это требование. Это означает, что различие в поведении компиляторов, которое вы наблюдаете, основывается на их интерпретациях стандартов C++ и на том, насколько последовательно они применяют обновления.

Таким образом, в соответствии с последними изменениями в стандарте, можно утверждать, что NTTP-строительство происходит в случае, когда это необходимо для определения соответствующих спецификаций, и если при этом применяемое значение не «эквивалентно» (template-argument-equivalent) исходному объекту, то это приведёт к ошибке компиляции.

Оцените материал
Добавить комментарий

Капча загружается...