在C++98中,使用类模板时,即使从使用中可以明显看出它们的类型,也总是必须提供模板参数:
std::pair<int, std::string> p(1729, "taxicab");
随着C ++ 11中auto的引入,情况发生了一些变化。可以使用(预先存在的)辅助函数make_pair创建变量,从而避免重复类型:
#include
#include
#include
#include
int main() {
std::pair p(1729, "taxicab");// deduces to std::pair p(1729, "taxicab");
static_assert(std::is_same_v<decltype(p), std::pair<int, const char *>>);
//std::tuple t(4, 3, 2.5); // same as auto t = std::make_tuple(4, 3, 2.5);
auto t = std::make_pair(4, 2.5);
static_assert(std::is_same_v<decltype(t), std::pair<int, double>>);
auto tt = std::make_tuple(4, 3, 2.5);
static_assert(std::is_same_v<decltype(tt), std::tuple<int, int, double>>);
std::less l; // same as std::less l;
}
但是,这依赖于make_pair
函数模板的存在,因此,如果您希望为自己的类模板提供类似的功能,则必须确保可以使用辅助函数。使用语言规则只是一种习惯用法,该语言规则允许在调用函数模板时推导出模板参数。
类模板参数推导的基础是函数模板参数推导。如果使用模板名称创建对象,但未指定任何模板参数,则编译器将构建一个虚构的“构造函数模板”集,称为推导指南,并对函数模板使用通常的重载解析和参数推导规则。
推导指南集包括一些自动生成的指南,以及(可选)一些用户定义的指南。
编译器基本上为主类模板的每个构造函数生成一个推导指南。虚构构造函数模板的模板参数是类模板参数加上构造函数可能具有的所有模板参数。功能参数按原样使用。对于std::pair其中的一些虚构函数模板,其外观应如下所示:
template <class T1, class T2>
constexpr auto pair_deduction_guide() -> std::pair<T1, T2>;
template <class T1, class T2>
auto pair_deduction_guide(std::pair<T1, T2> const& p) -> std::pair<T1, T2>;
template <class T1, class T2>
constexpr auto pair_deduction_guide(T1 const& x, T2 const& y) -> std::pair<T1, T2>;
template <class T1, class T2, class U1, class U2>
constexpr auto pair_deduction_guide(U1&& x, U2&& y) -> std::pair<T1, T2>;
template <class T1, class T2, class U1, class U2>
constexpr auto pair_deduction_guide(std::pair<U1, U2> const& p) -> std::pair<T1, T2>;
//etc...
用户定义的推导指南必须在与其应用的类模板相同的范围内进行定义。它们看起来与我上面为自动指南编写的伪代码非常相似。
它们看起来很像是带有尾随返回类型的函数签名,但没有auto返回类型-这可以被认为与也不具有返回类型的构造函数的语法一致。
以下是STL中最常见的两种推导指南:迭代器和完善转发。
MyVec
类似于std :: vector
,因为它以元素类型T
为模板,但是可以从迭代器类型Iter
构造。调用范围构造函数可提供所需的类型信息,但是编译器可能无法实现Iter
与T
之间的关系。这就是推导指南的作用。
在类模板定义之后,语法模板
告诉编译器“当您为MyVec
运行CTAD
时,尝试对签名MyVec(Iter,Iter)
执行模板参数推导。如果成功,则要构造的类型为MyVec
。这实质上是取消引用迭代器类型以获得所需的元素类型。
#include
#include
template <typename T> struct MyVec {
template <typename Iter> MyVec(Iter, Iter) { }
};
template <typename Iter> MyVec(Iter, Iter) -> MyVec<typename std::iterator_traits<Iter>::value_type>;
int main() {
int * ptr = nullptr;
MyVec v(ptr, ptr);
static_assert(std::is_same_v<decltype(v), MyVec<int>>);
}
另一种情况是完美转发,其中MyAdvancedPair
具有与std :: pair
一样的完美转发构造函数。再次,编译器发现A
和B
与T
和U
是不同的类型,并且不知道它们之间的关系。在这种情况下,我们需要应用的变换是不同的:我们需要衰减decay(如果您不熟悉衰减decay,则可以跳过此操作)。有趣的是,我们不需要decay_t,尽管如果我们想要额外的冗长性,可以使用类型萃取。相反,推导指南模板
足够了。
这告诉编译器“当您为MyAdvancedPair
运行CTAD
时,尝试对签名MyAdvancedPair(X,Y)
执行模板参数推导,就好像它是按值获取参数一样。这种推论进行衰减decay。如果成功,则要构造的类型为MyAdvancedPair
。
这证明了有关CTAD和推导指南的关键事实。 CTAD会查看类模板的构造函数及其推导指南,以确定要构造的类型。这种推论要么成功(确定唯一类型),要么失败。一旦选择了要构造的类型,就可以通过重载解析来确定要调用的构造函数。
CTAD不会影响构造函数的调用方式。对于MyAdvancedPair(和std :: pair)
,推导指南的签名(名义上按值接受参数)会影响CTAD选择的类型。之后,重载解析将选择完美的转发构造函数,该构造函数通过完美转发来接受其参数,就像类类型已使用显式模板参数编写一样。
CTAD和推导指南也不具干扰性。为类模板添加推导指南不会影响现有代码,以前提供原始模板参数是必需的。这就是为什么我们能够在不破坏一行用户代码的情况下为多种STL类型添加推理指南的原因。
#include
#include
template <typename A, typename B> struct MyAdvancedPair {
template <typename T, typename U> MyAdvancedPair(T&&, U&&) { }
};
template <typename X, typename Y> MyAdvancedPair(X, Y) -> MyAdvancedPair<X, Y>;
int main() {
MyAdvancedPair adv(1729, "taxicab");
static_assert(std::is_same_v<decltype(adv), MyAdvancedPair<int, const char *>>);
}
在极少数情况下,您可能希望推导指南拒绝某些代码。这是std :: array
的工作方式:
#include
#include
template <typename T, size_t N> struct MyArray {
T m_array[N];
};
template <typename First, typename... Rest> struct EnforceSame {
static_assert(std::conjunction_v<std::is_same<First, Rest>...>);
using type = First;
};
template <typename First, typename... Rest> MyArray(First, Rest...)
-> MyArray<typename EnforceSame<First, Rest...>::type, 1 + sizeof...(Rest)>;
int main() {
MyArray a = { 11, 22, 33 };
static_assert(std::is_same_v<decltype(a), MyArray<int, 3>>);
}
像std :: array
一样,MyArray
是没有实际构造函数的聚合,但是CTAD
仍可通过推导指南为这些类模板工作。MyArray
的指南对MyArray(First,Rest ...)
执行模板参数推导,将所有类型强制设置为相同,并根据有多少个参数确定数组的大小。
类似的技术可用于使CTAD对于某些构造函数或所有构造函数完全不正确。不过,STL本身并不需要明确地执行此操作。 (只有两类不需要使用CTAD
:unique_ptr
和shared_ptr
。C ++ 17支持数组的unique_ptrs
和shared_ptrs
,但是new T
和new T [N]
都返回T *
。因此,没有足够的信息来安全地推断由原始指针构造的unique_ptr
或shared_ptr
的类型。碰巧的是,由于unique_ptr
对花式指针fancy pointers
的支持和shared_ptr
对类型擦除的支持,这在STL中被自动阻止,这两种方式都以防止CTAD工作的方式更改了构造函数签名。)
这里有一些不被模仿的高级示例;相反,它们只是用来说明CTAD在复杂情况下的工作方式。编写函数模板的程序员最终将了解“非推论上下文”。
例如,类型名称为Identity
的函数模板不能从该函数参数推导出T
。现在,CTAD
已经存在,非推论上下文也将影响类模板的构造函数。
C:\Temp>type corner1.cpp
template <typename X> struct Identity {
using type = X;
};
template <typename T> struct Corner1 {
Corner1(typename Identity<T>::type, int) { }
};
int main() {
Corner1 corner1(3.14, 1729);
}
C:\Temp>cl /EHsc /nologo /W4 /std:c++17 corner1.cpp
corner1.cpp
corner1.cpp(10): error C2672: 'Corner1': no matching overloaded function found
corner1.cpp(10): error C2783: 'Corner1 Corner1(Identity::type,int)' : could not deduce template argument for 'T'
corner1.cpp(6): note: see declaration of 'Corner1'
corner1.cpp(10): error C2641: cannot deduce template argument for 'Corner1'
corner1.cpp(10): error C2514: 'Corner1': class has no constructors
corner1.cpp(5): note: see declaration of 'Corner1'
在corner1.cpp
中,类型名Identity
阻止编译器推断T
应该为双精度。
在这种情况下,某些(但不是全部)构造函数在非推论上下文中提及T:
C:\Temp>type corner2.cpp
template <typename X> struct Identity {
using type = X;
};
template <typename T> struct Corner2 {
Corner2(T, long) { }
Corner2(typename Identity<T>::type, unsigned long) { }
};
int main() {
Corner2 corner2(3.14, 1729);
}
C:\Temp>cl /EHsc /nologo /W4 /std:c++17 corner2.cpp
corner2.cpp
corner2.cpp(11): error C2668: 'Corner2::Corner2' : ambiguous call to overloaded function
corner2.cpp(7): note: could be 'Corner2::Corner2(double,unsigned long)'
corner2.cpp(6): note: or 'Corner2::Corner2(T,long)'
with
[
T=double
]
corner2.cpp(11): note: while trying to match the argument list '(double, int)'
在corner2.cpp
中,CTAD
成功,但是构造函数重载解析失败。由于未推导上下文,CTAD
忽略构造函数采用(类型名Identity
,无符号长整数),因此CTAD
仅使用(T,long)
进行推导。像任何函数模板一样,将参数(T,Long)
与参数类型double
进行比较,int
推导T
为double
。 (int
可以转换为long
,这足以推导模板参数;它在那里不需要完全匹配。)在CTAD
确定应构造Corner2
之后,构造函数重载解析会在替换后同时考虑两个签名(double,long)
和(double,unsigned long)
,而对于double
类型,int
参数类型则是模棱两可的(因为int
可同时转换为long
和unsigned long
,并且Standard不喜欢转换)。
C:\Temp>type corner3.cpp
#include
template <typename T> struct Corner3 {
Corner3(T) { }
template <typename U> Corner3(U) { }
};
#ifdef WITH_GUIDE
template <typename X> Corner3(X) -> Corner3<X *>;
#endif
int main() {
Corner3 corner3(1729);
#ifdef WITH_GUIDE
static_assert(std::is_same_v<decltype(corner3), Corner3<int *>>);
#else
static_assert(std::is_same_v<decltype(corner3), Corner3<int>>);
#endif
}
CTAD的工作方式是对从类模板的构造函数和推导指南生成的一组推导候选(假设功能模板)执行模板参数推导和重载解析。特别是,这遵循了过载解析的通常规则,仅增加了两个。重载解析仍然喜欢更专业的东西(N4713 16.3.3 [over.match.best] /1.7)当事物同样专业时,会有一个新的决胜局:扣除指南是首选(/1.12)。
在corner3.cpp
中,没有推导指南,Corner3(T)
构造函数用于CTAD
(而Corner3(U)
并未用于CTAD
,因为它没有提到T),并且构造了Corner3
。添加推演指南后,Corner3(T)
和Corner3(X)
签名具有同等的专业性,因此/1.12段介入并更喜欢演绎指南。这表示构造Corner3
然后使用U = int
调用Corner3(U)
)。
原文链接: https://devblogs.microsoft.com/cppblog/how-to-use-class-template-argument-deduction/