模板参数推导是指模板函数的参数类型,或类模板构造函数(从 C++17 开始),足够清晰,编译器能够在不使用模板参数的情况下理解。这个特性有一定的规则,但大多是直观的。
如何做到这一点…
一般来说,当你使用具有明确兼容参数的模板时,模板参数推导会自动发生。让我们看一些例子。
在函数模板中,参数推导通常是这样的:
template<typename T>
const char * f(const T a) {
return typeid(T).name();
}
int main() {
cout << format("T is {}\n", f(47));
cout << format("T is {}\n", f(47L));
cout << format("T is {}\n", f(47.0));
cout << format("T is {}\n", f("47"));
cout << format("T is {}\n", f("47"s));
}
输出:
T is int
T is long
T is double
T is char const *
T is class std::basic_string<char...
因为类型很容易辨别,所以在函数调用中没有必要指定模板参数,如 f<int>(47)。编译器可以从参数中推导出 `<int>` 类型。
注意
上述输出显示了有意义的类型名称,大多数编译器会使用缩写,如 i 代表 int,PKc 代表 const char * 等。
这对于多个模板参数同样有效:
template<typename T1, typename T2>
string f(const T1 a, const T2 b) {
return format("{} {}", typeid(T1).name(), typeid(T2).name());
}
int main() {
cout << format("T1 T2: {}\n", f(47, 47L));
cout << format("T1 T2: {}\n", f(47L, 47.0));
cout << format("T1 T2: {}\n", f(47.0, "47"));
}
输出:
T1 T2: int long
T1 T2: long double
T1 T2: double char const *
这里编译器推导出了 T1 和 T2 的类型。
注意,类型必须与模板兼容。例如,你不能从字面量中获取引用:
template<typename T>
const char * f(const T& a) {
return typeid(T).name();
}
int main() {
int x{47};
f(47); // 这将不会编译
f(x); // 但这个会
}
从 C++17 开始,你还可以在类中使用模板参数推导。所以现在这将有效:
pair p(47, 47.0); // 推导为 pair<int, double>
tuple t(9, 17, 2.5); // 推导为 tuple<int, int, double>
这消除了对 std::make_pair() 和 std::make_tuple() 的需求,因为现在你可以在不明确指定模板参数的情况下直接初始化这些类。为了向后兼容,std::make_* 辅助函数将继续保持可用。
它是如何工作的…
让我们定义一个类,看看这是如何工作的:
template<typename T1, typename T2, typename T3>
class Thing {
T1 v1{};
T2 v2{};
T3 v3{};
public:
explicit Thing(T1 p1, T2 p2, T3 p3)
: v1{p1}, v2{p2}, v3{p3} {}
string print() {
return format("{}, {}, {}\n",
typeid(v1).name(),
typeid(v2).name(),
typeid(v3).name()
);
}
};
这是一个具有三种类型和三个相应数据成员的模板类。它有一个 print() 函数,返回一个格式化字符串,包含三个类型名称。
没有模板参数推导,我将不得不像这样实例化此类的一个对象:
Things<int, double, string> thing1{1, 47.0, "three"}
现在我可以这样做:
Things thing1{1, 47.0, "three"}
这既更简单,又减少了出错的可能性。
当我在 thing1 对象上调用 print() 函数时,我得到这个结果:
cout << thing1.print();
输出:
int, double, char const *
当然,你的编译器可能会报告一些有效的相似内容。
在 C++17 之前,模板参数推导不适用于类,因此你需要一个辅助函数,它可能看起来像这样:
template<typename T1, typename T2, typename T3>
Things<T1, T2, T3> make_things(T1 p1, T2 p2, T3 p3) {
return Things<T1, T2, T3>(p1, p2, p3);
}
auto thing1(make_things(1, 47.0, "three"));
cout << thing1.print();
输出:
int, double, char const *
STL 包括一些这样的辅助函数,如 make_pair() 和 make_tuple() 等。这些现在已过时,但将为了与旧代码兼容而保持。
还有更多…
考虑具有参数包的构造函数的情况:
template <typename T>
class Sum {
T v{};
public:
template <typename... Ts>
Sum(Ts&& ... values) : v{ (values + ...) } {}
const T& value() const { return v; }
};
注意构造函数中的折叠表达式(values + ...)。这是 C++17 的特性,它将运算符应用于参数包的所有成员。在这种情况下,它将 v 初始化为参数包的总和。
这个类的构造函数接受任意数量的参数,其中每个参数可能是不同的类。例如,我可以这样调用它:
Sum s1 { 1u, 2.0, 3, 4.0f }; // 无符号整数,双精度浮点数,整数,单精度浮点数
Sum s2 { "abc"s, "def" }; // std::string,C 字符串
当然,这不会编译。模板参数推导未能找到所有不同参数的共同类型。我们得到一个错误消息,大意是:
无法推导 'Sum' 的模板参数
我们可以使用模板推导指南来修复这个问题。推导指南是一种辅助模式,用于帮助编译器进行复杂推导。这是我们构造函数的指南:
template <typename... Ts>
Sum(Ts&& ... ts) -> Sum<std::common_type_t<Ts...>>;
这告诉编译器使用 std::common_type_t 特性,它尝试为包中的所有参数找到一个共同类型。现在我们的参数推导工作了,我们可以看到它确定了哪些类型:
Sum s1 { 1u, 2.0, 3, 4.0f }; // 无符号整数,双精度浮点数,整数,单精度浮点数
Sum s2 { "abc"s, "def" }; // std::string,C 字符串
auto v1 = s1.value();
auto v2 = s2.value();
cout << format("s1 is {} {}, s2 is {} {}",
typeid(v1).name(), v1, typeid(v2).name(), v2);
输出:
s1 is double 10, s2 is class std::string abcdef