在c++ 17之前,总是必须显式地指定类模板的所有模板参数类型。
例如,:
std::complex<double> c{5.1,3.3};//不能省略double
std::mutex mx;
std::lock_guard<std::mutex> lg(mx);//std::muext也不能省略
c++ 17起,必须显式指定模板参数的约束得到了放宽。如果构造函数能够推导出所有模板参数,则可以跳过显式定义模板参数:
现在可以如下声明:
std::complex c{5.1,3.3};
std::mutex mx;
std::lock_guard lg(mx);
1. 使用类模板参数推断
可以使用类模板参数推断,只要传递给构造函数的参数可以用来推断类模板参数。推演支持所有的初始化方法(假设初始化本身有效):
std::complex c1(1.1, 2.2); // deduces std::complex<double>
std::complex c2{2.2, 3.3}; // deduces std::complex<double>
std::complex c3 = 3.3; // deduces std::complex<double>
std::complex c4 = {4.4}; // deduces std::complex<double>
c3和c4的初始化是可能的,因为可以通过只传递一个参数来初始化std::complex<>,这足以推导出模板参数T,然后将其用于实部和虚部:
namespace std
{
template<typename T>
class complex
{
constexpr complex(const T& re = T(), const T& im = T());
...
}
};
但是,请注意模板参数必须是明确可推断的。因此,下面的初始化不起作用:
std::complex c5{5,3.3}; // ERROR: attempts to int and double as T
对于模板,通常没有用于推断模板参数的类型转换。
还支持变量模板的类模板参数推断。例如,对于std::tuple<>,定义为:
namespace std
{
template<typename... Types>
class tuple;
};
std::tuple t{42, 'x', nullptr};//推演出t的类型是std::tuple<int, char, std::nullptr_t>.
还可以推断出非类型模板参数。例如,我们可以从一个传递的初始数组中推断出元素类型和大小的模板参数如下:
例1:
#include <iostream>
template<typename T, int SZ>
class MyClass
{
public:
MyClass(T(&t)[SZ])
{
}
};
int main()
{
MyClass mc("hello"); // deduces T as const char and SZ as 6
return 0;
}
这里我们将SZ的大小推演为6,因为传递的模板参数是一个包含6个字符的字符串文本,t的类型如下:
甚至可以推断用作重载基类的lambdas的类型[后续文件会讲],或者推断自动模板参数的类型[后续文件会讲]。
1.1默认的拷贝
如果类模板参数推断可以解释为初始化一个副本,那么它更喜欢这种解释。例如,初始化一个只有一个元素的std::vector之后:
std::vector v1{42}; // vector<int> with one element
使用该vector作为另一个vector的初始化器,被解释为创建一个副本:
std::vector v2{v1}; // v2 also is vector<int>
同样,这适用于所有有效的初始化形式:
std::vector v3(v1); // v3 also is vector<int>
std::vector v4 = {v1}; // v3 also is vector<int>
auto v5 = std::vector{v1}; // v3 also is vector<int>
只有当多个元素被传递,从而不能将其解释为创建副本时,初始化器列表的元素才定义新vector的元素类型:
std::vector vv{v, v}; // vv is vector<vector<int>>
这就提出了一个问题,当传递可变参数模板时,类模板参数推演会发生什么:
#include <iostream>
#include <vector>
template<typename... Args>
auto make_vector(const Args& ... elems)
{
return std::vector{ elems... };
}
int main()
{
std::vector<int> v{ 1, 2, 3 };
auto x1 = make_vector(v, v); // vector<vector<int>>
auto x2 = make_vector(v);// vector<int> or vector<vector<int>> ???
return 0;
}
在这里x2的类型到底是什么呢?目前,在visual studio 2019编译器处理这个问题的结果是vector<int>,在gcc9.2.0编译器中是std::vector<std::vector<int>>类型。
1.2 lambda表达式的类型推演
通过类模板参数的推导,我们第一次可以用lambda的类型实例化类模板(确切地说:lambda的闭包类型)。例如,我们可以提供一个通用类,包装和计数调用的回调:
例 3:
#include <iostream>
#include <vector>
#include <utility>
#include <algorithm>
template<typename CB>
class CountCalls
{
private:
CB callback; // callback to call
long calls = 0; // counter for calls
public:
CountCalls(CB cb) : callback(cb)
{
}
template<typename... Args>
auto operator() (Args&& ... args)
{
++calls;
return callback(std::forward<Args>(args)...);
}
long count() const
{
return calls;
}
};
void display(const std::vector<int> vi)
{
for (const auto v : vi)
{
std::cout << v << " ";
}
std::cout << std::endl;
}
int main()
{
std::vector v{ 1, 5, 4, 2, 3 };
CountCalls sc([](auto x, auto y)
{
return x > y;
});
std::sort(v.begin(), v.end(), std::ref(sc));
std::cout << "sorted with " << sc.count() << " calls\n";
display(v);
std::cout << "-------------------------" << std::endl;
auto fo = std::for_each(v.begin(), v.end(),
CountCalls([](auto i) {
std::cout << "elem: " << i << '\n';
}));
std::cout << "output with " << fo.count() << " calls\n";
}
在这里,构造函数使用回调函数进行包装,允许将其类型作为模板参数CB进行推导。例如,我们可以初始化一个对象传递一个lambda作为参数:
CountCalls sc([](auto x, auto y) {
return x > y;
});
这意味着排序标准sc的类型被推断为CountCalls<TypeOfTheLambda>。这样,我们可以计算这个例子中传递的排序条件的调用次数:
std::sort(v.begin(), v.end(),std::ref(sc));
std::cout << "sorted with " << sc.count() << " calls\n";
这里,包装好的lambda被用作排序条件,但是它必须通过引用传递,否则std::sort()只使用它自己传递的副本的计数器,因为std::sort()本身按值接受排序条件。
结果如下:
但是,我们可以将一个包装好的lambda传递给std::for_each(),因为这个算法(在非并行版本中)返回它自己的已传递回调的副本,以便能够使用它的结果状态:
auto fo = std::for_each(v.begin(), v.end(),
CountCalls([](auto i) {
std::cout << "elem: " << i << '\n';
}));
std::cout << "output with " << fo.count() << " calls\n";
1.3不支持部分类模板参数的推导
注意,与函数模板不同,类模板参数可能不能仅部分参数推导(通过显式地只指定一些模板参数)。
例 5:
#include <iostream>
#include <string>
template<typename T1, typename T2, typename T3 = T2>
class C
{
public:
C(T1 x = T1{}, T2 y = T2{}, T3 z = T3{})
{
}
};
int main(void)
{
// all deduced:
C c1(22, 44.3, "hi"); // OK: T1 is int, T2 is double, T3 is const char*
C c2(22, 44.3); // OK: T1 is int, T2 and T3 are double
C c3("hi", "guy"); // OK: T1, T2, and T3 are const char*
// only some deduced:
//C<std::string> c4("hi", "my"); // ERROR: only T1 explicitly defined
//C<> c5(22, 44.3); // ERROR: neither T1 not T2 explicitly defined
//C<> c6(22, 44.3, 42); // ERROR: neither T1 nor T2 explicitly defined
// all specified:
C<std::string, std::string, int> c7; // OK: T1,T2 are string, T3 is int
C<int, std::string> c8(52, "my"); // OK: T1 is int,T2 and T3 are strings
C<std::string, std::string> c9("a", "b", "c"); // OK: T1,T2,T3 are strings
return 0;
}
注意,第三个模板参数有一个默认值。因此,如果指定了第二种类型,则不需要显式地指定最后一种类型。
结果如下:
如果你想知道为什么不支持部分专门化,下面是导致这个决定的例子:
std::tuple<int> t(42, 43); // still ERROR
tuple是一个可变参数模板,因此可以指定任意数量的参数。因此,在本例中,不清楚只指定一种类型是错误的,还是故意这样做。至少看起来是有问题的。C++标准委员会在经过很长时间的讨论之后,仍然将部分专门化添加到了标准c++中。
不幸的是,不支持部分专门化意味着无法解决一些常见的编码需求。我们仍然不能很方便地使用lambda来指定关联容器的排序条件或无序容器的哈希函数:
std::set<Cust> coll([](const Cust& x, const Cust& y) // still ERROR
{
return x.name() > y.name();
});
我们还必须指定lambda的类型,例如:
auto sortcrit = [](const Cust& x, const Cust& y)
{
return x.name() > y.name();
};
std::set<Cust, decltype(sortcrit)> coll(sortcrit); // OK
1.4 类模板参数推演代替便捷函数
原则上使用类模板参数推导,我们可以不能只是为了能够从传递的参数方便类型推导而提供的便利函数模板。
一个明显的例子是make_pair(),它允许不指定传递参数的类型。例如对于std::vector<int> v,我们可以用如下代码
auto p = std::make_pair(v.begin(), v.end());
替换掉:
std::pair<typename std::vector<int>::iterator,
typename std::vector<int>::iterator> p(v.begin(), v.end());
从C++17起这里不再需要make_pair(),因为我们现在可以简单地声明:
std::pair p(v.begin(), v.end());
然而,std::make_pair()也是一个很好的例子,它演示了有时方便函数所做的不仅仅是推导模板参数。事实上,std::make_pair()也会推导,这特别意味着传递的string字面值类型被转换成const char*:
auto q = std::make_pair("hi", "world"); // pair of pointers
在本例中,q的类型是std::pair<const char*, const char*>。
通过使用类模板参数推导,有些情况下事情变得更加复杂。让我们看看一个简单的类声明的相关部分,比如std::pair:
template<typename T1, typename T2>
struct Pair1
{
T1 first;
T2 second;
Pair1(const T1& x, const T2& y) : first{x}, second{y}
{
}
}
当参数通过引用传递的。根据规则,当通过引用传递模板类型的参数时,参数类型不会衰减。所以,当调用:
Pair1 p1{"hi", "world"}; // deduces pair of arrays of different size, but...compile error
T1推导为char[3], T2推导为char[6]。原则上,这样的推论是合法的。T1和T2声明的成员first和second被声明为:
char first[3];
char second[6];
然而我们知道,不允许从数组的左值初始化数组,对于上面的代码编译器试图按照如下代码去编译:
const char x[3] = "hi";
const char y[6] = "world";
char first[3] {x}; // ERROR
char second[6] {y}; // ERROR
所以,这里编译器会报如下错误:
注意,通过值传递声明参数,就不会有这个问题:
template<typename T1, typename T2>
struct Pair2
{
T1 first;
T2 second;
Pair2(T1 x, T2 y) : first{x}, second{y}
{
}
};
对于Pair2这种类型:
Pair2 p2{"hi", "world"}; // deduces pair of pointers
T1和T2都可以推导为const char*:
由于标准库中声明了类std::pair<>,以便构造函数引用这些参数,所以现在可能希望下面的初始化不会编译通过:
std::pair p{"hi", "world"}; // seems to deduce pair of arrays of different size, but...compile ok
在C++17中这个是可以编译的,因为可以使用推导指南。