这个问题是由于疯狂的好奇心而不是实际问题。 请考虑以下代码:
template<typename...>
struct type_list {};
template<typename, typename = void>
struct test_class;
template<typename... T>
struct test_class<type_list<T...>> {
static constexpr auto value = false;
};
template<typename T>
struct test_class<type_list<T>, std::enable_if_t<std::is_same_v<T, int>>> {
static constexpr auto value = true;
};
int main() {
static_assert(!test_class<type_list<double, char>>::value);
static_assert(test_class<type_list<int>>::value);
}
此操作失败并显示错误:
"test_class
"的模棱两可的部分专业化
如果我将第二个专业化更改为从功能角度来看不起作用的东西,错误就会消失:
template<typename T>
struct test_class<type_list<T>> {
static constexpr auto value = true;
};
同样,如果我使用别名模板void_t
,一切都按预期工作:
template<typename T>
struct test_class<type_list<T>, std::void_t<std::enable_if_t<std::is_same_v<T, int>>>> {
static constexpr auto value = true;
};
除了将void_t
和enable_if_t
结合起来的丑陋之外,当存在与int
不同的单一类型时,这也完成了工作,即对于static_assert(!test_class<type_list<char>>::value)
(由于显而易见的原因,它在第二种情况下不起作用)。
我明白为什么第三种情况有效,因为当满足enable_if_t
的条件并且type_list<T>
比type_list<T...>
更专业时,别名模板实际上被替换为void
(对吧? 但是,我本来也期望以下情况相同:
template<typename T>
struct test_class<type_list<T>, std::enable_if_t<std::is_same_v<T, int>>> {
static constexpr auto value = true;
};
归根结底,当条件被统计时,std::enable_if_t<std::is_same_v<T, int>>
以某种方式void
(好吧,从技术上讲,它是typename blabla::type
的,当然,但::type
不是仍然void
吗?),因此我不明白为什么它会导致模棱两可的电话。我很确定我在这里错过了一些明显的东西,我现在很好奇地理解它。
如果您能指出这方面的标准,并让我知道是否存在比最终将void_t
和enable_if_t
结合起来更好的解决方案,我会很高兴。
让我们从代码的扩展版本开始
template<typename, typename = void>
struct test_class;
template<typename T>
struct test_class<type_list<T>> {
static constexpr auto value = false;
};
template<typename... Ts>
struct test_class<type_list<Ts...>> {
static constexpr auto value = false;
};
template<typename T>
struct test_class<type_list<T>, std::enable_if_t<std::is_same_v<T, int>>> {
static constexpr auto value = true;
};
称为
test_class<type_list<int>>::value
试试这里!
该标准区分了等效的模板参数、仅在功能上等效的模板参数和不等效的模板参数[temp.over.link]/5
如果包含模板参数的两个函数定义满足一个定义规则,则涉及模板参数的两个表达式被视为等效,但只要将用于在一个表达式中命名模板参数如果对于任何给定的模板参数集,表达式的计算的令牌替换为在另一个表达式中命名相同模板参数的另一个令牌,用于命名模板参数的标记可能会有所不同。 如果包含表达式的两个函数定义满足一个定义规则,则两个不涉及模板参数的未计算操作数被视为等效,只是用于命名类型和声明的标记可能不同,只要它们命名相同的实体,并且只要两个模板 ID 相同,用于形成概念 ID 的标记就可能不同 ([temp.type])。
结果相同,则涉及不等效的模板参数的两个潜在计算表达式在功能上是等效的。 如果对于任何给定的模板参数集,表达式以相同的顺序对相同的实体执行相同的操作,则两个不等效的未计算操作数在功能上是等效的。
例如std::enable_if_t<std::is_same_v<T, T>>
和void
仅在功能上等效:将计算第一个项以void
任何模板参数T
。这意味着根据包含两个专门的 [temp.over.link]/7 代码<T, void>
和<T, std::enable_if_t<std::is_same_v<T, T>>
已经格式不正确:
如果程序的有效性或含义取决于两个构造是否等效,并且它们在功能上等效但不等价,则程序格式不正确,无需诊断。
在上面的代码中,std::enable_if_t<std::is_same_v<T, int>>
在功能上甚至不等同于任何其他版本,因为它通常不等同于void
.
当现在执行部分排序[temp.func.order] 以查看哪个专用化与您的调用最匹配时,这将导致歧义,因为test_class
在两种情况下都是同样专门的 [temp.func.order]/6(Ts={int}, void
或T=int, std::enable_if_t<std::is_same_v<T, int>>
,两者都会导致int, void
但您无法相互推断它们),因此编译将失败。
另一方面,通过用std::void_t
包裹std::enable_if_t
,这只不过是 void 的别名
template <typename T>
using void_t = void;
部分排序将成功,因为在这种情况下,编译器已经知道最后一个模板参数的类型在所有情况下都是void
的,选择T=int
test_class<T, std::void_t<std::enable_if_t<std::is_same_v<T,int>>>>
作为最专业的,因为非可变参数情况(T=int, void
)被认为比可变参数Ts={int}, void
更专业(参见例如temp.deduct.partial/8或再次参见这个)。