在C++17之前,你必须明确指出类模板的所有参数,例如:
complex<double> c{5.1,3.3};
mutex mx;
lock_guard<mutex> lg(mx);
C++17起必须指明类模板参数的限制被放宽了。通过类模板参数推导,只要编译器能够根据初始值推导模板参数,就可以不指名参数。
例如:
complex c{5.1,3.3};
mutex mx;
lock_guard lg(mx);
vector v1{1,2,3};
只要能根据初始值推导出所有模板参数就可以使用类模板参数推导。
complex c1{1.1,2.2};
complex c2(1.1,2.2);
complex c3 = 3.3;
complex c4 = {4.4};
但如果推导过程中有歧义,是不能通过编译的。
complex c5{5,3.3};
对于可变参数模板也可以使用类模板参数推导,例如tuple
:
tuple t{42,'x',nullptr}; // 推导为int,char,nullptr_t
也可以推导非类型模板参数。例如:
template<typename T, int SZ>
class MyClass
{
public:
MyClass (T(&) [SZ])
{}
};
MyClass mc("hello"); // T为const char, SZ为6
类模板参数推导过程中首先尝试以拷贝的方式初始化。
vector v1{42};
vector v2{v1}; // 会推导为vector而不是vector>
但如果传递的是两个vector
来初始化,那么会被推导为vector
。
vector vv{v1,v2}; // 会推导为vector>
通过使用类模板参数推导,我们可以用lambda
的类型作为模板参数来实例化类模板。例如:
// 对于一个回调函数进行包装,并统计调用次数
template<typename CB>
class CountCalls
{
private:
CB callback; // 回调函数
long calls = 0; // 调用次数
public:
CountCalls(CB cb) : callback(cb) {}
template<typename...Args>
decltype(auto) operator()(Args&&... args)
{
++calls;
return callback(forward<Args>(args)...);
}
long count() const
{
return calls;
}
};
int main()
{
CountCalls sc{ [](auto x,auto y) { return x > y; } };
vector v{ 1,7,2,5,6,9,3 };
sort(v.begin(), v.end(), ref(sc)); // 必须引用传递,否则只会修改拷贝的计数器
cout << sc.count() << "calls" << endl;
// for_each会返回传入的回调函数
auto fo = for_each(v.begin(), v.end(), CountCalls{ [](auto i) {
cout << "elem: " << i << " ";
} });
cout << endl;
cout << "output with " << fo.count() << "calls" << << endl;
}
输出结果如下:
39 calls
elem: 19
elem: 17
elem: 13
elem: 11
elem: 9
elem: 7
elem: 5
elem: 3
elem: 2
output with 9 calls
注意,不像函数模板,类模板不能只指明一部分模板参数,希望编译器去推导剩余的部分参数。
template<typename T1, typename T2, typename T3 = T2>
class C
{
public:
C(T1 x = {}, T2 y = {}, T3 z = {})
{}
};
int main()
{
C c1(22, 44.3, "hi"); // OK: int,double,const char*
C c2(22, 44.3); // OK: int,double,double
C c3("hi", "guy"); // OK: T1=T2=T3=const char*
C<string> c4("hi", "my"); // ERROR
C<> c5(22, 44.3); // ERROR
C<> c6(22, 44.3, 42); // ERROR
}
为什么不支持部分参数推导,这里有一个导致这个决定的例子:
tuple<int> t(42,43); // ERROR
std::tuple
是一个可变参数模板,因此你可以指明任意数量的模板参数。在这个例子中,并不能判断出只指明一个参数是一个错误还是故意的。
快捷函数就是通过传入的参数实例化相应的类模板,一个明显的例子make_pair()
,能够帮我们避免指明传入参数的类型,但现在已经不需要这种了,例如:
vector<int> v;
auto p = make_pair(v.begin(),v.end());
pair p(v.begin(),v.end());
你可以定义特定的推导指引
来给类模板参数添加新的推导或者修正构造函数定义的推导。例如:
在没有定义特定的推导指引之前,Pair p1{"hi","world"}
会将T1推导为char[3]
,T2推导为char[6]
。
template<typename T1,typename T2>
struct Pair
{
T1 first;
T2 second;
Pair(const T1& x, const T2& y) : first{ x }, second{ y }
{}
};
// 为构造函数定义推导指引
template<typename T1,typename T2>
Pair(T1, T2) -> Pair<T1, T2>;
在->
的左侧我们声明了我们想要推导声明。这里我们声明的是使用两个以值传递且类型分别为T1
和T2
的对象创建一个Pair
对象。在->
的右侧,定义了推导的结果。也就是说,推导的类型会根据传入的值本身的类型做决定,不会被const &
所影响。上面的例子就会推导T1
和T2
为const char*
。
重载推导规则的一个非常重要的用途就是确保模板参数T在推导时发生退化
。
template<typename T>
struct C
{
C(const T&) {}
};
如果我们传递了一个字面量"hello"
,传递的类型是const char(&)[6]
,因此,T就会被推到为char[6]
。
但只要进行简单的定义推导指引:
template<typename T>
C(T) -> C<T>;
推导指引不一定用于模板,也不一定用于构造函数。
template<typename T>
struct S
{
T val;
};
S(const char*) -> S(string);
但推导出来的结果是const char*
,那么就会被推到为string
类型。
推导指引会和类的构造函数产生竞争。类模板参数推导时会根据重载情况选择最佳匹配的构造函数/推导指引。如果一个构造函数和推导指引匹配的优先级一样,那么就会优先匹配推导指引。
推导指引可以用explicit
声明。当出现explicit
不允许的初始化或转换时这一条推导指引就会被忽略。例如:
template<typename T>
struct S
{
T val;
};
explicit S(const char*) -> S<string>;
如果使用拷贝初始化,将会忽略这一条推导指引。
S s1 = {"hello"}; // 无效
S s2{"hello"}; // OK
S s3 = S{"hello"}; // OK
S s4 = {S{"hello"}}; // OK
另一个例子如下:
template<typename T>
struct Ptr
{
Ptr(T) { cout << "Ptr(T)\n"; }
template<typename U>
Ptr(U) { cout <<"Ptr(U)\n"; }
};
template<typename T>
explicit Ptr(T) -> Ptr<T*>
Ptr p1{42}; // Ptr
Ptr p2 = 42; // Ptr
int i = 42;
Ptr p2{&i}; // Ptr
Ptr p4 = &i; // Ptr
泛型聚合体中也可以使用推导指引来支持类模板参数推导。例如,对于:
template<typename T>
struct A
{
T val;
};
在没有推导指引的情况下使用类模板推导会导致错误:
A i1{42}; // ERROR
A s1{"hello"}; // ERROR
C++17在标准库引入了很多推导指引。
pair
和tuple
的推导指引:
// pair
template<typename T1,typename T2>
pair(T1,T2) -> pair<T1,T2>;
// tuple
template<typename... Types>
tuple(Types...) -> tuple<Types...>;
迭代器推导指引:
// vector
template<typename Iterator>
vector(Iterator,Iterator) -> vector<typename iterator_traits<Iterator>::value_type>;
array
推导指引:
template<typename T,typename... U>
array(T,U...) -> array<enable_if_t<(is_same_v<T,U> && ...), T>,(1+sizeof...(U))>; // is_same_v确保所有参数类型一致
map
推导指引:
// 定义
namespace std
{
template<typename Key,typename T,typename Compare = less<Key>,
typename Allocator = allocator<pair<const Key,T>>>
class map {
// ...
};
}
// 推导指引
template<typename Key, typename T, typename Compare = less<Key>,
typename Allocator = allocator<pair<const Key, T>>>
map(initializer_list<pair<Key, T>>, Compare = Compare(), Allocator = Allocator()) ->
map<Key, T, Compare, Allocator>;
智能指针不存在推导指引:
shared_ptr sp{new int(7)}; // ERROR
上边的写法是错误的。
通过语法if constexpr
可以计算编译期的条件表达式来在编译期决定使用if
语句的哪一个部分。其余部分的代码将不会被生成,但是还是会做语法检查。
template<typename T>
string asString(T x)
{
if constexpr (is_same_v<T, string>)
return x;
else if constexpr (is_arithmetic_v<T>)
return to_string(x);
else
return string(x);
}
int main()
{
cout << asString(42) << '\n';
cout << asString(string("hello")) << '\n';
cout << asString("hello") << '\n';
}
template<typename T>
string asString(T x)
{
if (is_same_v<T, string>)
return x;
else if (is_arithmetic_v<T>)
return to_string(x);
else
return string(x);
}
这段代码不能通过编译,因为模板在实例化时整个模板会作为一个整体进行编译。然而if语句的条件表达式都是运行时特性。即使在编译期就能确定条件表达式的值一定是false,then的部分也必须能够通过编译。因此,当传递一个string
的时候,会因为to_string
无效而导致编译失败。
编译期if可能会影响函数的返回值类型。例如:
auto foo()
{
if constexpr (sizeof(int) < 4)
return 42;
else
return 42u;
}
如果使用运行期的if可能将永远不能通过编译,因为推导返回值类型会考虑到所有可能的返回值类型。
有的时候,如果在if语句就返回了,可以跳过else语句部分,也就是说:
auto foo()
{
if()
{
return a;
}
else
{
return b;
}
}
auto foo()
{
if()
{
return a;
}
return b;
}
这两种方式是等价的,但是,第二种写法如果用在编译期的if可能会导致错误。
auto foo()
{
if constexpr(sizeof(int) > 4)
return 42;
return 42u;
}
编译器会推导两个不同的返回值类型,会导致错误。
在考虑如下代码:
template<typename T>
constexpr auto foo(const T& val)
{
if constexpr (is_integral_v<T>)
{
if constexpr (T{} < 10)
return val * 2;
}
return val;
}
int main()
{
constexpr auto x1 = foo(42);
constexpr auto x2 = foo("hi");
}
上面的代码没有问题,但是如果将两个if语句的条件表达式写在一起,并使用&&
来判断,可能会导致短路现象。
template<typename T>
constexpr auto foo(const T& val)
{
if constexpr (is_integral_v<T> && T{} < 10)
{
return val * 2;
}
return val;
}
int main()
{
constexpr auto x1 = foo(42);
constexpr auto x2 = foo("hi"); // ERROR
}
然而,编译期的if的条件表达式总是作为整体实例化并且整体有效,这就意味着如果传入的是一个不能<10
的运算将不能通过编译。
因此,要写成嵌套if constexpr
而不是&&
连接。
编译期if的一个应用就是先对返回值进行一些处理,再进行完美转发。因为decltype(auto)
不能推导为void
,因此:
#include
#include
template<typename Callable,typename... Args>
decltype(auto) call(Callable op, Args&&... args)
{
if constexpr (is_void_t<invoke_result_t<Callable, Args...>>)
{
// 返回值类型是void
op(forward<Args>(args)...);
return;
}
else
{
// 返回值不是void
decltype(auto) ret{ op(forward<Args>(args)...) };
return ret;
}
}
编译期if的一个典型应用是类型分发
。在C++17之前,你必须为每一个想处理的类型重载一个单独的函数。现在,有了编译期if,你可以把所有的逻辑放在一个函数里。
例如,重载版本的advance()
算法:
template<typename Iterator,typename Distance>
void advance(Iterator& pos, Distance n)
{
using cat = iterator_traits<Iterator>::iterator_category;
advanceImpl(pos, n, cat{});
}
template<typename Iterator, typename Distance>
void advanceImpl(Iterator& pos, Distance n, random_access_iterator_tag)
{
pos += n;
}
template<typename Iterator, typename Distance>
void advanceImpl(Iterator& pos, Distance n, bidirectional_iterator_tag)
{
if (n >= 0)
{
while (n--)
++pos;
}
else
{
while (n++)
--pos;
}
}
template<typename Iterator, typename Distance>
void advanceImpl(Iterator& pos, Distance n, input_iterator_tag)
{
while (n--)
++pos;
}
我们可以将所有的实现都放在同一个函数中:
template<typename Iterator, typename Distance>
void advance(Iterator& pos, Distance n)
{
using cat = iterator_traits<Iterator>::iterator_category;
if constexpr (is_convertible_v<cat, random_access_iterator_tag>)
{
pos += n;
}
else if constexpr (is_convertible_v<cat, bidirectional_iterator_tag>)
{
if (n >= 0)
{
while (n--)
++pos;
}
else
{
while (n++)
--pos;
}
}
else
{
while (n--)
++pos;
}
}
注意编译期if语句也可以使用新的带初始化的方式。例如,有一个constexpr
函数foo()
,你可以这样写:
template<typename T>
void bar(const T x)
{
if constexpr (auto obj = foo(x); is_same_v<decltype(obj), T>)
{
cout << "foo(x) yields same type\n";
}
else
{
cout << "foo(x) yields different type\n";
}
}
如果有一个参数类型也为T的constexpr
函数foo()
,可以根据返回的值来判定,可以这么写:
constexpr auto c = 1;
constexpr foo(constexpr auto c);
if constexpr(constexpr auto obj = foo(c); obj == 0)
{
cout << "foo() == 0\n";
}
if constexpr
可以在任何函数中使用,并非仅限于模板。只要条件表达式是编译期的,并且可以转换为bool
类型。
C++17起,有一个新的特性可以计算对参数包中所有参数应用一个二元运算符的结果。
例如,下面的函数将会返回所有参数的总和:
template<typename... T>
auto foldSum(T... args)
{
return (... + args); // ((arg1) + (arg2) + arg3)
}
返回语句的括号是折叠表达式的一部分,不可省略。
注意折叠表达式参数的位置很重要,如果反着写:
(... + args) // (arg1 + (arg2 + arg3))
折叠表达式的出现让我们不必递归实例化模板的方式来处理参数包。在C++17之前:
template<typename T>
auto foldSumRec(T arg)
{
return arg;
}
template<typename T1,typename... Ts>
auto foldSumRec(T1 arg1, Ts... otherArgs)
{
return arg1 + foldSumRec(otherArgs...);
}
这样的实现不仅麻烦,而且编译器也很难处理。
给定一个参数args
和一个操作符op
,C++17语法规范:
一元左折叠
( ... op args ) => ( ( arg1 op arg2 ) op arg3 ) op ...
一元右折叠
( args op ... ) => arg1 op ( arg2 op ... ( argN-1 op argN ) )
当使用折叠表达式处理空参数包时,将遵循如下规则:
&&
运算符,值为true
。||
运算符,值为false
。逗号
运算符,值为void()
。对于其他情况,我们可以添加一个初始值value
,一个参数包args
,一个操作符op
,C++17语法规范:
二元左折叠
( value op ... op args ) => ( ( ( value op arg1) op arg2 ) op arg3 ) op ...
二元右折叠
( args op ... op value ) => arg1 op ( arg2 op ... ( argN op value ) )
例如,下面的定义在进行加法时允许传入一个空参数包:
template<typename... T>
auto foldSum(T... s)
{
return (0 + ... + s); // sizeof...(s) == 0 也能执行
}
有的时候第一个操作数是特殊的,比如:
template<typename... T>
void print(const T&... args)
{
(cout << ... << args) << '\n';
}
这里,传递给print()
的第一个参数输出之后将返回输出流,所以后面的参数可以进行输出。但是,其他的实现可能会发生一些意料之外的结果,例如:
cout << (args << ... << '\n');
类似像print(1)
是可以调用,但会打印出1
左移'\n'
位的结果,'\n'
的ASCII码是10,结果为1024。
注意这个例子,两个参数之间没有输出空格字符。可能会导致输出结果观看效果极差,我们可以使用一个辅助函数来完成。
template<typename T>
const T& spaceBefore(const T& arg)
{
cout << ' ';
return arg;
}
template<typename First,typename... Args>
void print(const First& firstArg, const Args&... args)
{
cout << firstArg; // 当只有一个参数的时候,不需要打印空格
(cout << ... << spaceBefore(args)) << '\n';
}
这里的折叠表达式会展开成:
cout << spaceBefore(arg1) << spaceBefore(arg2) << ... << '\n';
我们也可以使用lambda
表达式来在print()
定义spaceBefore()
:
template<typename First,typename... Args>
void print(const First& firstArg, const Args&... args)
{
cout << firstArg; // 当只有一个参数的时候,不需要打印空格
auto outWithSpace = [](const auto& arg)
{
cout << ' ' << arg;
};
(..., outWithSpace(args));
}
你可以对除了.
、->
、[]
之外的所有二元运算符使用折叠表达式。
折叠函数调用:
// 对每个参数调用foo()函数
template<typename T>
void foo(const T& t);
template<typename... Types>
void callFoo(const Types&... args)
{
(..., foo(args)); // foo(arg1), foo(arg2), ...
}
另外,也可以支持移动语义:
template<typename... Types>
void callFoo(const Types&&... args)
{
(..., foo(forward<Types>(args))); // foo(arg1), foo(arg2), ...
}
如果foo()
函数的返回类型重载了,
运算符,那么代码行为可能会改变。不过可以将返回值转换为void()
。
template<typename... Types>
void callFoo(const Types&&... args)
{
(..., (void)foo(forward<Types>(args))); // foo(arg1), foo(arg2), ...
}
组合哈希函数:
template<typename T>
void hashCombine(size_t& seed, const T& val)
{
seed ^= hash<T>()(val) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
}
template<typename... Types>
size_t combinedHashValue(const Types&... args)
{
size_t seed = 0; // 初始化seed
(..., hashCombine(seed, args));
return seed;
}
有了这些定义,我们可以轻易的定义出一个新的哈希函数,并将这个函数用于某一个类型。
struct CustomerHash
{
size_t operator()(const Customer& c) const
{
return combinedHashValue(c.getFirstname(), c.getLastname(), c.getValue());
}
};
unordered_set<Customer, CustomerHash> coll;
unordered_map<Customer,string, CustomerHash> map;
折叠基类的函数调用:
template<typename... Bases>
class MultiBase : private Bases...
{
public:
void print()
{
// 调用基类所有的print()函数
(..., Bases::print());
}
};
struct A
{
void print()
{
cout << "A::print()\n";
}
};
struct B
{
void print()
{
cout << "B::print()\n";
}
};
struct C
{
void print()
{
cout << "C::print()\n";
}
};
int main()
{
MultiBase<A, B, C> mb;
mb.print();
}
折叠路径遍历:
使用折叠表达式遍历一个二叉树的路径。
struct Node
{
int value;
Node* Left{ nullptr };
Node* Right{ nullptr };
Node(int i = 0) : value{ i }
{}
int getValue() const
{
return value;
}
static constexpr auto left = &Node::Left;
static constexpr auto right = &Node::Right;
template<typename T,typename... Tp>
static Node* traverse(T np, Tp... paths)
{
return (np->* ...->*paths); // np->*paths1->*paths2
}
};
int main()
{
Node* root = new Node{ 0 };
root->Left = new Node{ 1 };
root->Left->Right = new Node{ 2 };
Node* node = Node::traverse(root, Node::left, Node::right);
cout << node->getValue() << '\n';
node = root->*Node::left->*Node::right;
cout << node->getValue() << '\n';
}
通过使用类型特征,我们也可以使用折叠表达式来处理模板参数包。例如:
// 检查所有类型是否相同
template<typename T1,typename... Tn>
struct IsHomogeneous
{
static constexpr bool value = (is_same_v<T1, Tn> && ...); // is_same_v && is_same_v && ...
};
C++一直在放宽对模板参数的标准。C++17也是如此。
非类型模板参数只能是常量整数值、对象/函数/成员的指针、对象或函数的左值引用、nullptr_t
类型。
对于指针,C++17之前需要外部或者内部链接。C++17之后,可以使用无链接的指针。
template<const char* str>
class Message
{};
extern const char hello[] = "Hello World!"; // 外部链接
const char hello1[] = "Hello World!"; // 内部链接
void foo()
{
Message<hello> msg; // OK
Message<hello1> msg1; // C++11起OK
static const char hello2[] = "Hello World"; // 无链接
Message<hello2> msg2; // C++17起OK
Message<"hi"> msg3; // ERROR
}
C++17起,可以使用占位符类型auto
和decltype(auto)
作为非类型模板参数的类型。
C++17起,可以使用auto
来声明非类型模板参数:
template<auto N>
class S
{};
但是,在实例化的时候,不能实例化一些不允许作为非类型模板参数的参数:
S<2.5> s; // ERROR
这个特性的一个应用就是你可以定义一个即可能是字符也可能是字符串的模板参数。例如:
template<auto Sep = ' ',typename First,typename... Args>
void print(const First& first, const Args&... args)
{
cout << first;
auto outWithSep = [](const auto& arg)
{
cout << Sep << arg;
};
(..., outWithSep(args));
cout << endl;
}
我们可以自定义每个输出结果直接的间隔,可以是默认的空格,也可以是其他字符或者字符串。
auto
模板参数特性的另一个应用是可以让我们更轻易的定义编译期常量。
template<typename T,T v>
struct constant
{
static constexpr T value = v;
};
int main()
{
using i = constant<int, 42>;
using c = constant<char, 'x'>;
using b = constant<bool,true>;
}
可以简单实现为:
template<auto v>
struct constant
{
static constexpr auto value = v;
};
int main()
{
using i = constant<42>;
using c = constant<'x'>;
using b = constant<true>;
}
可以使用auto
来实现变量模板。
例如:
template<typename T, auto N>
array<T, N> arr{};
int main()
{
arr<int,5>[0] = 17;
arr<int,5>[3] = 42;
arr<int,5u>[1] = 11;
arr<int,5u>[3] = 3;
}
arr
和arr
是不同的变量。
decltype(auto)
是C++14引入的,现在也可以作为模板参数。如果用这个来推导表达式而不是变量名,那么推导的结果将依赖于表达式的值类型:
type
。type&
。type&&
。例如:
template<decltype(auto) N>
struct S
{
void printN() const
{
cout << "N:" << N << endl;
}
};
static const int c = 42;
static int v = 42;
int main()
{
S<c> s1; // N=const int
S<(c)> s2; // N=const int&
s1.printN();
s2.printN();
S<(v)> s3; // int&
v = 77;
s3.printN();
}
using
声明支持逗号分割的名称列表,也可以用于参数包。
class Base
{
public:
void a();
void b();
void c();
};
class Derived : private Base
{
public:
using Base::a,Base::b,Base::c;
};
创建一个重载的lambda
的集合。通过如下定义:
// 继承所有基类的函数调用运算符
template<typename... Ts>
struct overload : Ts...
{
using Ts::operator()...;
};
// 基类的类型从传入的参数中推导
template<typename... Ts>
overload(Ts...)->overload <Ts...>;
int main()
{
auto twice = overload{
[](string& s) { s += s; },
[](auto& v) { v *= 2; }
};
int i = 42;
twice(i); // 84
string s = "hi";
twice(s); // hihi
}
除了出个声明继承构造函数之外,现在还支持如下方式:可以声明一个可变参数类模板Multi
,继承每一个参数类型的基类:
template<typename T>
class Base
{
T value{};
public:
Base() {}
Base(T v) : value{ v } {}
};
template<typename... Types>
class Multi : private Base<Types>...
{
public:
using Base<Types>::Base...;
};