了解默认模板参数何时是重新定义以及何时不在 SFINAE 技术中

bol*_*lov 5 c++ templates overloading sfinae language-lawyer

SFINAE 技术是使用默认类来启用/禁用功能。但是,这不适用于重载函数,导致“模板参数重新定义默认参数”:

template <class T, class = std::enable_if_t<std::is_integral_v<T>>>
auto foo(T) { return 1; }

template <class T, class = std::enable_if_t<std::is_floating_point_v<T>>>
auto foo(T) { return 2; }

// error template parameter redefines default argument"
Run Code Online (Sandbox Code Playgroud)

常见的解决方案是使用默认的非类型模板参数:

template <class T, std::enable_if_t<std::is_integral_v<T>, int> = 0>
auto foo(T) { return 1; }

template <class T, std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
auto foo(T) { return 2; }

// works
Run Code Online (Sandbox Code Playgroud)

这仅在条件“不同”时才有效。但是理解什么时候条件“不同”并不是那么简单。

从最明显的例子开始(相同(token by token)条件):

template <class T, std::enable_if_t<std::is_integral_v<T>, T> = 0>
auto foo(T) { return 1; }

template <class T, std::enable_if_t<std::is_integral_v<T>, T> = 0>
auto foo(T) { return 2; }

// error template parameter redefines default argument"
Run Code Online (Sandbox Code Playgroud)

使用两个不同的非依赖条件,评估为相同的值:

constexpr bool true_1 = true;
constexpr bool true_2 = true;

template <class T, std::enable_if_t<true_1, T> = 0>
auto foo(T) { return 1; }

template <class T, std::enable_if_t<true_2, T> = 0>
auto foo(T) { return 2; }

// error template parameter redefines default argument"
Run Code Online (Sandbox Code Playgroud)

使用两个不同的依赖条件,评估为相同的值:

template <class T> constexpr bool true_1 = true;
template <class T> constexpr bool true_2 = true;

template <class T, std::enable_if_t<true_1<T>, T> enable = 0>
auto foo(T) { return 1; }

template <class T, std::enable_if_t<true_2<T>, T> enable = 0>
auto foo(T) { return 2; }

// works
// (of course will give ambiguous call error when trying to call it,
//  but the point here is you are allowed to declare them like this)
Run Code Online (Sandbox Code Playgroud)

在最后一个示例中,如果我调用foo(即foo(24))两个重载具有完全相同的参数和模板参数:

错误:对“foo”的调用不明确

return foo(12);
       ^~~
Run Code Online (Sandbox Code Playgroud)

注意:候选函数[with T = int, enable = 0]

auto foo(T) { return 1; }
     ^
Run Code Online (Sandbox Code Playgroud)

注意:候选函数[with T = int, enable = 0]

auto foo(T) { return 2; }
     ^
Run Code Online (Sandbox Code Playgroud)

这似乎有效地实例化了两个相同的重载(根据声明,而不是定义)。

我所有的问题都非常密切相关,所以我在这里问他们所有人:

  1. 当两个非类型模板参数不被视为重定义时,确切的规则是什么?
  2. 标准如何处理两个声明相同的重载(如上一个示例)
  3. 为什么这适用于非类型模板参数,而不适用于类型模板参数?

小智 1

首先:

\n
// #1 Two templates with default type parameters.\ntemplate <typename T, typename T2 = int>\nvoid Foo(T) {}\ntemplate <typename T, typename T2 = char>   // Error \xe2\x80\x93 redefinition.\nvoid Foo(T) {}\n\n// #2 Two templates with default non-type parameters.\ntemplate <typename T, int = 0>\nvoid Foo(T) {}\ntemplate <typename T, char = 'x'>          // Allowed, but ambiguous for Foo<type>(value).\nvoid Foo(T) {}\n
Run Code Online (Sandbox Code Playgroud)\n

为什么它不适用于类型参数?

\n

#1 和 #2 不相等。前两个模板参数 T2 类似(允许任何类型),只是默认值(类型)不同。

\n

此外,标准规定:

\n
\n

可用的默认模板参数集是通过\n合并来自模板的所有先前声明的默认参数而获得的,\n其方式与默认函数参数 (11.3.6) 相同。\n示例:

\n
\n
template<class T1, class T2 = int> class A;\ntemplate<class T1 = int, class T2> class A;\n
Run Code Online (Sandbox Code Playgroud)\n
\n

相当于

\n
\n
template<class T1 = int, class T2 = int> class A;\n
Run Code Online (Sandbox Code Playgroud)\n

同一位置的多个默认类型不能共存。

\n

为什么它适用于非类型参数?

\n

使用非类型参数,您实际上可以创建具有具体类型(类似于专业化)的单独模板,该模板从一开始就为这些函数创建单独的签名。

\n

阐明你的例子

\n
constexpr bool true_1 = true;\nconstexpr bool true_2 = true;\n// Shorter version of second parameter, nameless.\ntemplate <class T, std::enable_if_t<true_1>* = 0> auto foo(T) { return 1; }\ntemplate <class T, std::enable_if_t<true_2>* = 0> auto foo(T) { return 2; }\n
Run Code Online (Sandbox Code Playgroud)\n

这不起作用,因为两个常量实际上总是相同的“true”,为非类型参数产生相同的类型。

\n
template <class T> constexpr bool true_1 = true;\ntemplate <class T> constexpr bool true_2 = true;\ntemplate <class T, std::enable_if_t<true_1<T>>* = 0> auto foo(T) { return 1; }\ntemplate <class T, std::enable_if_t<true_2<T>>* = 0> auto foo(T) { return 2; }\n
Run Code Online (Sandbox Code Playgroud)\n

该示例提供了不确定性因子 - T。当编译器只看到函数的声明时,它无法知道它将用什么来实例化它foo<>。可能有专业化true_2会指向错误。因此true_1true_2被视为不同的类型。

\n