请考虑以下 C++14 代码:
#include <type_traits>
template<typename T>
class Bar {
static_assert(std::is_destructible<T>::value, "T must be destructible");
};
template<typename T>
void foo(Bar<T> const &) {}
template<typename T>
void foo(T const &) {}
class Product {
public:
static Product *createProduct() { return new Product{}; }
static void destroyProduct(Product *product) { delete product; }
private:
Product() = default;
~Product() = default;
};
int main()
{
Product* p = Product::createProduct();
foo(*p); // call 1: works fine
foo<Product>(*p); // call 2: fails to compile
Product::destroyProduct(p);
return 0;
}
还有来自 clang 的错误消息:
error: static_assert failed due to requirement 'std::is_destructible<Product>::value' "T must be destructible"
static_assert(std::is_destructible<T>::value, "T must be destructible");
^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
note: in instantiation of template class 'Bar<Product>' requested here
foo<Product>(*p); // call 2: fails to compile
^
note: while substituting deduced template arguments into function template 'foo' [with T = Product]
foo<Product>(*p); // call 2: fails to compile
^
1 error generated.
我的理解是
call 1
和 call 2
都应该可以正常编译,但是 call 2
不仅无法在 clang 上编译,而且无法在 gcc 和 msvc 上编译。
从标准角度看,
call 1
编译成功而call 2
编译失败,这样正确吗?为什么?
注意:我知道我可以通过在
std::enable_if
的第一个重载中添加 foo
来解决该错误,但我想了解为什么 call 1
可以,但 call 2
不行。
替换失败不属于错误限制。如果故障发生在“直接上下文”之外,则故障很困难并且 SFINAE 无法阻止。
当您输入
foo<Product>(
?)
时,它会首先创建一个重载集。该过载集包括 foo
的两个过载。但是创建 foo 的第一个重载会导致在直接上下文之外发生static_assert
失败,因此是一个硬错误。
当您输入
foo(
?)
时,它还会尝试创建一个重载集。两个模板均被考虑。它没有传入 T
,因此它尝试从参数中推导出 T
。
论证是
Product&
。从 T const&
推出 Product&
会产生 T=Product const
。从 Bar<T> const&
推导出 Product&
... 无法推导出 T
,因为 Product
及其任何基类都不是从 Bar<typename>
形式的模板生成的。
如果没有推导出
T
,则 foo(Bar<T> const&)
过载将被丢弃。无法推断 T=Product
,因此尝试实例化 foo(Bar<Product> const&)
不会发生硬错误。
简而言之,对于调用 2,您手动强制存在
Bar<Product>
可能性。对于调用 1,编译器尝试推断模板参数,但永远无法实现。
作为一个思想实验,考虑将
Product
的声明更改为:
class Product: public Bar<int> {
其他一切保持不变。
现在
foo(*p)
将生成 2 个要考虑的过载 - 一个
template<class T=Product const>
void foo(Product const&)
还有一个
template<class T=int>
void foo(Bar<int> const&)
第二个是通过查看
Product
的基类并找到 Bar<int>
生成的。在一种情况下,T
被推导为 Product
,在另一种情况下,int
。
但是当你这样做
foo<Product>
时,你并不是依靠演绎来找到 T
- 你正在手动设置它。没有任何扣除可做。
在
foo(*p)
中,由于Product
不是Bar<T>
,因此没有实例化void foo(Bar<T> const &)
。
唯一可能的过载是
template<typename T> void foo(T const &)
。
在
foo<Product>(*p)
中,您指定模板参数,因此有以下候选者
foo<Product>(const Bar<Product>&)
foo<Product>(const Product&)
要知道
Product
是否可以转换为 Bar<Product>
,您必须实例化它,从而产生硬错误。
编译器可能会实例化该模板,即使它不会被重载解析所采用。
foo(可能)实例化 Bar,这可能是匹配的(但事实并非如此)。但是 Bar 会导致硬错误。