Вопрос или проблема
Это (частично) сокращено из 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) исходному объекту, то это приведёт к ошибке компиляции.