您可以定义特定的推导指南来提供额外的或修正现有的类模板参数演绎。例如,您可以定义,每当推导出一个Pair3的类型时,类型推断的操作应该像类型将通过值传递一样:
template
struct Pair3
{
T1 first;
T2 second;
Pair3(const T1& x, const T2& y) : first{x}, second{y}
{
}
};
推导指南如下:
// deduction guide for the constructor:
template
Pair3(T1, T2) -> Pair3;
在这里,->的左侧是声明默认情况下我们想要通过什么方式去推导。在这个例子中,它是通过值传递的任意类型T1和T2的两个对象创建一个Pair3。在->的右侧定义了推导的结果。在本例中,Pair3是用T1和T2这两种类型实例化的。你可能会认为这也是构造函数所做的。但是,构造函数通过引用传递参数,这样传递的数组或字符串字面值不会衰减。根据推导指南,模板参数会衰减,这意味着传递的数组或字符串字面值会衰减到相应的指针类型。
例如,如下声明:
Pair3 p3{"hi", "world"}; // deduces p3
尽管初始化字符串的类型是const char[3]和const char[6],但是推导指南规则将T1和T2推导为const char*。但是对于已经具体声明了模板类型参数的情况,声明的类型还是有效的,如:
Pair3 p3{"hi", "world"};// p3
构造函数仍然通过引用接受参数。推导指南只适用于推导模板类型。在具体声明T1和t2类型之后,它与实际构造函数调用无关。
例 1:
#include
template
struct Pair3
{
T1 first;
T2 second;
Pair3(const T1& x, const T2& y) : first{ x }, second{ y }
{
}
};
template
Pair3(T1, T2)->Pair3;
int main()
{
Pair3 p3{ "hi", "world" };
Pair3 p4{ "hi", "world" };
return 0;
}
p3和p4的类型如下:
如上例所示,通常,这些重载规则的一个非常有用的应用是确保模板参数T在推导过程中衰减。考虑一个典型的类模板:
#include
template
struct C
{
C(const T& t)
{
}
};
int main()
{
C c{"Hello"};//T deduced as const char[6], c deduced as [6]
return 0;
}
结果如下:
在这里T的类型为const char[6],原因是当参数通过引用传递时,模板参数推导不会转换到相应的指针类型。
有一个简单的推导指南:
template C(T) -> C;
得到结果如下:
现在,根据推导指南按值传递的参数,所以它的类型发生了衰减,“hello”将T推导为const char*类型。
因此,对于任何类模板,如果类模板的构造函数是引用模板参数的对象,那么相应的推导指南看起来都非常合理。
演绎指南不必是模板,也不必调用显式指定的构造函数。
例如,给出以下结构进行推导指南:
template
struct S
{
T val;
};
S(const char*) -> S; // map S<> for string literals to S
以下声明是可以的,其中T类型从const char*推断为std::string,因为传递的字符串字面值隐式地转换为它:
S s1{"hello"}; // OK, same as: S s1f"hello"g;
S s2 = {"hello"}; // OK, same as: S s2 = f"hello"g;
S s3 = S{"hello"}; // OK, both S deduced to be S
注意,聚合需要列表初始化(可以进行推理,但不允许初始化):
S s4 = "hello"; // ERROR (can’t initialize aggregates that way)
//compile Error (active) E0415 no suitable constructor exists to convert from "const char [6]" to "S"
//Error C2440 'initializing': cannot convert from 'const char [6]' to 'S'
完整例子如下:
#include
#include
template
struct S
{
T val;
};
S(const char*)->S; // map S<> for string literals to S
int main(void)
{
S s1{ "hello" }; // OK, same as: S s1f"hello"g;
S s2 = { "hello" }; // OK, same as: S s2 = f"hello"g;
S s3 = S{ "hello" }; // OK, both S deduced to be S
return 0;
}
结果如下:
类模板参数推导根据重载解析具有最高优先级的构造函数或推导指南。如果构造函数和推导指南同样匹配,则首选推导指南。
考虑如下定义:
#include
template
struct C1
{
C1(const T&)
{
}
};
C1(int)->C1;
int main()
{
C1 c1(10); // T deduced as long
C1 c2('a');//T deduced as char
return 0;
}
当传递一个int值给c1,由于推导指南不是模板,它更匹配,所以在这使用了推导指南。但是如果传入的是一个char类型的值,这里没有对char类型的推导指南,构造函数更加匹配,所以T被推导为char。
结果如下:
对于通过值传递参数和通过引用传递参数匹配度相同的情况,最好使用推导指南和引用来取参数,让推导指南按值传递参数通比较好(这也有转换的好处)。
推导指南可以显示声明,显示推导指南忽略转换初始化。例如:
#include
#include
template
struct S
{
T val;
};
explicit S(const char*)->S;
int main(void)
{
//S s1 = { "hello" }; // ERROR (deduction guide ignored and otherwise invalid)
S s2{ "hello" }; // OK, same as: S s1f"hello"g;
S s3 = S{ "hello" }; // OK
S s4 = {S{"hello"}}; // OK
return 0;
}
通过(非显示)推导指南传递参数的S对象的拷贝初始化(使用=)将忽略推导指南[有点像显示构造函数的初始化]。这里在等号=右边必须显示的指定推导指南,否则初始化无效:
S s1 = {"hello"}; // ERROR (deduction guide ignored and otherwise invalid)
直接初始化或在右侧显式推导仍然是可能的:
S s2{"hello"}; // OK, same as: S s1f"hello"g;
S s3 = S{"hello"}; // OK
S s4 = {S{"hello"}}; // OK
另一个例子,我们可以这样做:
#include
template
struct Ptr
{
Ptr(T) { std::cout << "Ptr(T)\n"; }
template
Ptr(U) { std::cout << "Ptr(U)\n"; }
};
template explicit Ptr(T)->Ptr;
int main(void)
{
Ptr p1{ 42 }; // deduces Ptr due to deduction guide
Ptr p2 = 42; // deduces Ptr due to constructor
int i = 42;
Ptr p3{ &i }; // deduces Ptr due to deduction guide
Ptr p4 = &i; // deduces Ptr due to constructor
return 0;
}
在这里p1,p3是因为显示推导指南更匹配,所以使用Ptr(U)。对于p2和p4,由于是通过拷贝初始化(使用=),显示UI推导指南不符合,因此使用构造函数。
可以在泛型聚合中使用推导指南,在泛型聚合中启用的是类模板参数推导。例如:
#include
#include
template
struct A
{
T val;
};
A(const char*)->A;
int main()
{
//A i1{ 42 }; // ERROR, if not declare decuction guides for aggregates
//A s1("hi"); // still error if declare decuction guides for aggregates
//A s2{ "hi" }; // ERROR, if not declare decuction guides for aggregates
//A s3 = "hi"; // still error if declare decuction guides for aggregates
//A s4 = { "hi" }; // ERROR, if not declare decuction guides for aggregates
A i2{ 42 }; //ok, if not declare decuction guides for aggregates
A s5 = { "hi" }; //ok, if not declare decuction guides for aggregates
A s6{ "hi" }; // OK, if declare decuction guides for aggregates
A s7 = { "hi" }; // OK, if declare decuction guides for aggregates
return 0;
}
如果没有推导指南,任何尝试类模板参数推导都是错误的。这种情况下,你必须明确地传递T类型的参数:
A i2{42};
A s5 = {"hi"};
但在使用推导指南后,如:
A(const char*) -> A;
可以按如下方式初始化聚合:
A s2{"hi"}; // OK
A s4 = {"hi"}; // OK
但是,与通常的聚合一样,仍然需要大括号。否则,虽然成功推导出类型T,但是这种初始化是错误的:
A s1("hi"); // ERROR: T is string, but no aggregate initialization
A s3 = "hi"; // ERROR: T is string, but no aggregate initialization
接下来介绍的std::array的推导指南是聚合推演指南的另一个例子。
c++标准库中使用c++ 17引入了一些推导指南。
在C++17以前,通过迭代器范围初始化的方式去初始化容器对象是,需要指明容器元素类型。例如:
#include
#include
#include
int main()
{
std::set sf;
std::vector vf(sf.begin(), sf.end());
return 0;
}
在这里通过sf的迭代器范围初始化方式去初始化了vf变量,但是在定义vf变量的时候需要明确指出容器元素的类型。从C++17 起,为了能够推导从迭代器范围初始化的方式中推导迭代器元素的类型,容器得有一个推导指南,对于std::vector<>有个犹如以下的推导指南:
// let std::vector<> deduce element type from initializing iterators:
namespace std
{
template
vector(Iterator, Iterator)
-> vector::value_type>;
}
这样,就可以允许我们写如下代码:
#include
#include
#include
int main()
{
std::set sf;
std::vector vf(sf.begin(), sf.end()); //OK, deduces std::vector
return 0;
}
一个更有趣的例子提供了类std::array<>,能够推导元素类型和元素数量:
#include
#include
int main(void)
{
std::array a{27, 42, 99}; // OK, deduces std::array
return 0;
}
变量a的推导类型如下:
对于std::array<>,定义了类似如下的推导指南:
// let std::array<> deduce their number of elements (must have same type):
namespace std
{
template
array(T, U...)
-> array && ...), T>,
(1 + sizeof...(U))>;
}
推导指南使用折叠表达式[后续文章介绍]
(is_same_v
为确保所有传递的参数的类型都是相同的。因此,如下声明是不正确的:
std::array a{42,45,77.7}; // ERROR: types differ
可以给具有key/value对的容器((map, multimap, unordered_map, unordered_multimap)定义推导指南。这些容器的元素类型为std::pair
因此,在C++17中对于std::map的方法可能如下:
#include
#include
结果如下:
注意,c++标准库中的一些地方没有推导指南,尽管你可能希望它们是可用的。例如,你可能希望为shared_ptr和unique_ptr提供推导指南,因此可以替换:
std::shared_ptr
为
std::shared_ptr sp{new int(7)}; // not supported
这种写法会编译错误,因为shared_ptr的构造函数是一个模板,因此没有隐式的推导指南使用
namespace std
{
template class shared_ptr
{
public:
...
template explicit shared_ptr(Y* p);
...
};
}
Y是不同于T的模板参数,因此不能从构造函数推导出Y的类型。这个特性的使用可能如下:
std::shared_ptr sp{new Derived(...)};//T is Base, Y is Derived
也许可以简单地提供一个相应的推导指南如下:
namespace std
{
template shared_ptr(Y*) -> shared_ptr;
}
然而,这也意味着在分配数组时要遵循以下指南:
std::shared_ptr sp{new int[10]}; // OOPS: would deduces shared_ptr
正如在c++中经常遇到的那样,我们遇到了一个棘手的C问题,即指向一个对象和一个对象数组的指针的类型具有或衰退为同一类型。因为这个问题看起来很危险,所以c++标准委员会决定(暂时)不支持它。你仍然需要调用单个对象:
std::shared_ptr sp1{new int}; // OK
auto sp2 = std::make_shared(); // OK
std::shared_ptr p(new std::string[10],
[](std::string* p) {
delete[] p;
});
或者
std::shared_ptr p(new std::string[10],
std::default_delete());