推断的过程
- 每个实参-参数对的推断都是独立的,如果结果矛盾推断就会失败
template<typename T>
const T& max(const T& a, const T& b)
{
return a < b ? b : a;
}
auto g = max(1, 1.0);
template<typename T>
typename T::E at(T a, int i)
{
return a[i];
}
void f(int* p)
{
int x = at(p, 7);
}
template<typename T>
const T& max(const T& a, const T& b);
max("Apple", "Pear");
推断上下文(Deduced Context)
template<typename T>
void f1(T*);
template<typename T, int N>
void f2(T(&)[N]);
template<typename T1, typename T2, typename T3>
void f3(T1 (T2::*)(T3*));
class S {
public:
void f(double*);
};
void g(int*** ppp)
{
bool b[42];
f1(ppp);
f2(b);
f3(&S::f);
}
- 复杂的类型声明的匹配过程从最顶层构造开始,然后不断递归子构造,即各种组成元素,这些构造被称为推断上下文。下列构造不能作为推断上下文用来推断模板参数T:
- 受限类型名称,如
A<T>::X
不能用来推断T
- 非类型表达式,如
A<N+1>
不能用来推断N
- 非推断上下文(nondeduced context)不代表程序是错误的
template<int N>
class X {
public:
using I = int;
void f(int) {}
};
template<int N>
void f(void (X<N>::*p)(typename X<N>::I));
int main()
{
f(&X<33>::f);
}
- 相反,参数类型完全依赖于推断上下文也可能导致推断矛盾
template<typename T>
void f(X<Y<T>, Y<T>>);
void g()
{
f(X<Y<int>, Y<int>>());
f(X<Y<int>, Y<char>>());
}
特殊的推断情况
template<typename T>
void f(T, T);
void (*pf)(char, char) = &f;
class S {
public:
template<typename T>
operator T&();
};
void f(int(&)[20]);
void g(S s)
{
f(s);
}
初始化列表(Initializer List)的推断
- 函数调用的实参是一个初始化列表时,实参没有具体类型,不会发生推断
#include <initializer_list>
template<typename T>
void f(T);
int main()
{
f({1, 2, 3});
}
#include <initializer_list>
template<typename T>
void f(std::initializer_list<T>);
int main()
{
f({2, 3, 5, 7, 9});
f({'a', 'e', 'i', 'o', 'u', 42});
}
参数包的推断
template<typename First, typename... Rest>
void f(First first, Rest... rest);
void g(int i, double j, int* k)
{
f(i, j, k);
}
template<typename T, typename U> class X {};
template<typename T, typename... Ts>
void f(const X<T, Ts>&...);
template<typename... Ts1, typename... Ts2>
void g(const X<Ts1, Ts2>&...);
void h(X<int, float> x1, X<int, double> x2, X<double, double> x3)
{
f(x1, x2);
g(x1, x2);
f(x1, x3);
g(x1, x3);
}
template<typename... Ts> class X { };
template<typename... Ts>
bool f(X<Ts...>, X<Ts...>);
template<typename... Ts1, typename... Ts2>
bool g(X<Ts1...>, X<Ts2...>);
void h(X<short, int> x1, X<unsigned short, unsigned> x2)
{
f(x1, x1);
g(x1, x1);
f(x1, x2);
g(x1, x2);
}
字面值运算符模板(Literal Operator Template)
template<char... cs>
int operator"" _B7()
{
std::array<char, sizeof...(cs)> chars{cs...};
for (char c : chars) std::cout << "'" << c << "'";
return 1;
}
int a = 3.14_B7;
- 这个技术只支持即使没有后缀(suffix)也有效的数字字面值
auto b = 01.3_B7;
auto c = 0xFF00_B7;
auto d = 0815_B7;
auto e = hello_B7;
auto f = "hello"_B7;
引用折叠(Reference Collapsing)
& + & → &
& + && → &
&& + & → &
&& + && → &&
using RCI = const int&;
volatile RCI&& r = 42;
using RRI = int&&;
const RRI&& rr = 42;
转发引用(Forwarding Reference)
- 当函数参数是一个转发引用时,模板实参推断不仅考虑函数调用实参类型,还会考虑实参是左值还是右值
- 实参为左值则参数类型推断为实参类型的左值引用,引用折叠规则将确保替换的参数是一个左值引用
- 实参为右值则参数类型推断为实参类型(不是引用类型),替换的参数是一个此类型的右值引用
template<typename T>
void f(T&& p);
void g()
{
int i;
const int j = 0;
f(i);
f(j);
f(2);
}
- 这隐藏着一个问题,若T为引用类型,模板定义中涉及未初始化的T类型就会出错
template<typename T>
void f(T&&)
{
T x;
}
template<typename T>
void f(T&&)
{
std::remove_reference_t<T> x;
}
完美转发
- 引用折叠的的规则使得函数模板可以接受几乎任何实参并捕获它的关键属性(类型以及是左值还是右值),由此函数模板能转发实参给另一个函数,这种技术称为完美转发
template<typename T>
void f(T&& x)
{
g(std::forward<T>(x));
}
- 完美转发也可以结合可变参数模板把任意数量实参转发给另一个函数
template<typename... Ts>
void f(Ts&&... args)
{
g(std::forward<Ts>(args)...);
}
- 完美转发处理空指针常量时会造成问题,整型值会被当作常量值0
void g(int*);
void g(...);
template<typename T>
void f (T&& x)
{
g(std::forward<T>(x));
}
void foo()
{
g(0);
f(0);
}
g(nullptr);
f(nullptr);
- 完美转发调用另一个函数的返回类型时,可以用decltype推断返回类型
template<typename... Ts>
auto f(Ts&&... args) -> decltype(g(std::forward<Ts>(args)...))
{
return g(std::forward<Ts>(args)...);
}
- C++14引入了decltype(auto)简化使用
template<typename... Ts>
decltype(auto) f(Ts&&... args)
{
return g(std::forward<Ts>(args)...);
}
template<typename T, unsigned N>
T* begin(T(&a)[N])
{
return a;
}
template<typename C>
typename C::iterator begin(C& c)
{
return c.begin();
}
int main()
{
std::vector<int> v;
int a[10];
::begin(v);
::begin(a);
}
- SFINAE只发生于函数模板替换的即时上下文中。函数模板替换时发生于以下实例化期间的事,以及由替换过程触发的特殊成员函数的任何隐式定义,都不属于即时上下文的部分
- 如果替换时使用了类成员,则需要类模板实例化,此期间发生的错误就不在即时上下文中,即使另一个函数模板匹配无误也不会使用SFINAE
template<typename T>
class Array {
public:
using iterator = T*;
};
template<typename T>
void f(Array<T>::iterator first, Array<T>::iterator last);
template<typename T>
void f(T*, T*);
int main()
{
f<int&>(0, 0);
}
- 使用auto返回类型,必须实例化定义来确定返回类型,实例化定义不属于即时上下文,此期间产生错误不会使用SFINAE
template<typename T>
auto f(T x)
{
return x->m;
}
int f(...);
template<typename T>
auto g(T x) -> decltype(f(x));
int main()
{
g(42);
}
推断的限制
template<typename T>
class B<T> {};
template<typename T>
class D : B<T> {};
template<typename T>
void f(B<T>*);
void g(D<long> x)
{
f(&x);
}
- C++17之前,模板实参推断不能用于类模板,不能从类模板的构造函数的实参推断类模板参数
template<typename T>
class X {
public:
X(T b) : a(b) {}
private:
T a;
};
X x(12);
- C++17允许类模板实参推断,注意如果使用这种推断就不能显式指定一部分参数,类模板的所有参数要么通过显式指定指出,要么通过实参推断推出,不能一部分使用显式指定一部分使用推断
template<typename T1, typename T2, typename T3 = T2>
class C {
public:
C(T1 x = T1{}, T2 y = T2{}, T3 z = T3{});
};
C c1(1, 3.14, "hi");
C c2(1, 3.14);
C c3("hi", "guy");
C c4;
C c5("hi");
C<string> c10("hi","my", 42);
C<> c11(1, 3.14, 2);
C<string, string> c12("hi","my");
template<typename T>
void f(T x = 42)
{}
int main()
{
f<int>();
f();
}
template<typename T>
void f(T, int) noexcept(nonexistent(T()));
template<typename T>
void f(T, ...);
void test(int i)
{
f(i, i);
}
template<typename T>
void g(T, int) throw(typename T::Nonexistent);
template<typename T>
void g(T, ...);
void test(int i)
{
g(i, i);
}
显式指定实参
int f(int);
template<typename T> T f(T);
auto x = f(42);
auto y = f<>(42);
- 在友元函数声明的上下文中,显式模板实参列表的存在有一个问题
void f();
template<typename>
void f();
namespace N {
class C {
friend int f();
friend int f<>();
};
}
template<typename T>
typename T::E f();
template<typename T>
T f();
auto x = f<int*>();
template<typename... Ts>
void f(Ts... args);
f<double, double, int>(1, 2, 3);
g<double, int>(1, 2, 3);
auto
template<typename C>
void f(const C& c)
{
auto it = c.begin();
while (it != c.end())
{
auto& x = *it++;
…
}
}
template<typename T>
void f2(T it);
f2(c.begin());
template<typename T>
void f3(T& x);
f3(*it++);
auto&& x = ...;
template<typename T>
void f(T&& x);
int i;
auto&& rr = 42;
auto&& lr = i;
- auto&&常用于绑定一个值类型(左值或右值)未知的对象
template<typename C>
void f(C c)
{
for (auto&& x: c) ...
}
auto f() { return 42; }
auto f() -> auto { return 42; }
- 如果没有显式声明lambda的返回类型,则lambda的返回类型默认被视为auto
[] (int x) { return f(x); };
[] (int x) -> auto { return f(x); };
auto f();
auto f() { return 42; }
int g();
auto g() { return 42; }
struct S {
auto f();
};
auto S::f() { return 42; }
- C++17之前,非类型模板实参必须用具体类型声明,它可以是模板参数类型
template<typename T, T N>
struct X;
X<int, 42>* x;
template<auto N>
struct X;
X<42>* p;
X<3.14>* q;
template<auto N>
struct X {
using Type = decltype(N);
};
template<typename>
struct PMClassT;
template<typename C, typename M>
struct PMClassT<M C::*> {
using Type = C;
};
template<typename PM>
using PMClass = typename PMClassT<PM>::Type;
template<auto PMD>
struct CounterHandle {
PMClass<decltype(PMD)>& c;
CounterHandle(PMClass<decltype(PMD)>& c): c(c) {}
void incr()
{
++(c.*PMD);
}
};
struct S {
int i;
};
int main()
{
S s{41};
CounterHandle<&S::i> h(s);
h.incr();
}
- 使用auto模板参数只要指定指向成员的指针常量
&S::i
作为模板实参,而C++17前必须指定冗长的具体类型
CounterHandle<int S::*, &S::i>
template<auto... VS>
struct Values {};
Values<1, 2, 3> beginning;
Values<1, 'x', nullptr> triplet;
template<auto V1, decltype(V1)... VRest>
struct X {};
decltype
- 如果e是一个实例名称(如变量、函数、枚举、数据成员)或一个类成员访问,decltype(e)产生实例或类成员实例的声明类型。当想匹配已有声明类型时
auto x = ...;
auto y1 = x + 1;
decltype(x) y2 = x + 1;
- 反之,如果e是其他表达式,decltype(e)产生一个反映表达式type或value的类型
- e是T类型lvalue,产生T&
- e是T类型xvalue,产生T&&
- e是T类型prvalue,产生T
int&& i = 0;
decltype(i)
decltype((i))
decltype(auto)
int i = 42;
const int& r = i;
auto x = r;
decltype(auto) y = r;
std::vector<int> v = { 42 };
auto x2 = v[0];
decltype(auto) y2 = v[0];
template<typename C>
class X {
C c;
decltype(auto) operator[](std::size_t n)
{
return c[n];
}
};
int i;
decltype(auto) x = i;
decltype(auto) r = (i);
int g() { return 1; }
decltype(auto) f()
{
int r = g();
return (r);
}
template<decltype(auto) Val>
class S {};
constexpr int c = 42;
extern int v = 42;
S<c> sc;
S<(v)> sv;
template<auto N>
struct S {};
template<auto N>
int f(S<N> p);
S<42> x;
int r = f(x);
template<auto V>
int f(decltype(V) p);
int r1 = deduce<42>(42);
int r2 = deduce(42);
auto推断的特殊情况
template<typename T>
void f(T);
f({ 1 });
f({ 1, 2, 3 });
template<typename T>
void g(std::initializer_list<T>);
g({ 1, 2, 3 });
- 对初始化列表的推断,auto会将其视为std::initializer_list,即上述第一种情形
auto x = { 1, 2 };
- C++14禁止了对auto用initializer_list直接初始化,必须用=
auto x { 1, 2 };
- 但允许单个元素的直接初始化,这和圆括号初始化一样,不会被推断为std::initializer_list
auto x { 1 };
auto f() { return { 1 }; }
char c;
auto *p = &c, d = c;
auto e = c, f = c + 1;
auto f(bool b)
{
if (b)
{
return 42.0;
}
else
{
return 0;
}
}
- 如果返回的表达式递归调用函数,则不会发生推断从而导致出错
auto f(int n)
{
if (n > 1)
{
return n * f(n - 1);
}
else
{
return 1;
}
}
auto f(int n)
{
if (n <= 1)
{
return 1;
}
else
{
return n * f(n - 1);
}
}
- auto返回类型没有对应的副本时会推断为void,若不能匹配void则出错
auto f1() {}
auto f2() { return; }
auto* f3() {}
template<typename T, typename U>
auto f(T t, U u) -> decltype(t+u)
{
return t + u;
}
void f(...);
template<typename T, typename U>
auto g(T t, U u) -> decltype(auto)
{
return t + u;
}
void g(...);
struct X {};
using A = decltype(f(X(), X()));
using B = decltype(g(X(), X()));
结构化绑定(Structured Binding)
- 结构化绑定是C++17引入的新特性,作用是在一次声明中引入多个变量。一个结构化绑定必须总有一个auto,可以用cv限定符或&、&&声明符
struct X { bool valid; int value; };
X g();
const auto&& [b, N] = g();
- 初始化一个结构化绑定,除了使用类类型,还可以使用数组、std::tuple-like的类(通过get<>绑定)
double pt[3];
auto& [x, y, z] = pt;
x = 3.0; y = 4.0; z = 0.0;
auto f() -> int(&)[2];
auto [ x, y ] = f();
auto& [ r, s ] = f();
std::tuple<bool, int> bi {true, 42};
auto [b, i] = bi;
int r = i;
std::tuple_element<i, E>::type& n_i = e.get<i>();
std::tuple_element<i, E>::type&& n_i = e.get<i>();
std::tuple_element<i, E>::type& n_i = get<i>(e);
std::tuple_element<i, E>::type&& n_i = get<i>(e);
#include <utility>
enum M {};
template<>
struct std::tuple_size<M> {
static unsigned const value = 2;
};
template<>
struct std::tuple_element<0, M> {
using type = int;
};
template<>
struct std::tuple_element<1, M> {
using type = double;
};
template<int> auto get(M);
template<> auto get<0>(M) { return 42; }
template<> auto get<1>(M) { return 7.0; }
auto [i, d] = M();
泛型lambada(Generic Lambda)
template<typename Iter>
Iter findNegative(Iter first, Iter last)
{
return std::find_if(first, last,
[] (typename std::iterator_traits<Iter>::value_type value) {
return value < 0;
});
}
template<typename Iter>
Iter findNegative(Iter first, Iter last)
{
return std::find_if(first, last,
[] (auto value) {
return value < 0;
});
}
- lambda创建时不知道实参类型,推断不会立即进行,而是先把模板类型参数添加到模板参数列表中,这样lambda就可以被任何实参类型调用,只要实参类型支持
<0
操作,结果能转为bool
[] (int i) {
return i < 0;
}
- 编译器把这个表达式编译成一个新创建类的实例,这个实例称为闭包(closure)或闭包对象(closure object),这个类称为闭包类型(closure type)。闭包类型有一个函数调用运算符(function call operator),因此闭包是一个函数对象。上面这个lambda的闭包类型就是一个编译器内部的类。如果检查一个lamdba的类型,std::is_class将生成true
class X {
public:
X();
bool operator() (int i) const
{
return i < 0;
}
};
foo(..., [] (int i) { return i < 0; });
foo(..., X{});
- 如果lambda要捕获局部变量,捕获将被视为关联类成员的初始化
int x, y;
[x,y] (int i) {
return i > x && i < y;
}
class Y {
public:
Y(int x, int y) : _x(x), _y(y) {}
bool operator() (int i) const
{
return i > _x && i < _y;
}
private:
int _x, _y;
};
- 对一个泛型lambda,函数调用操作符将变成一个成员函数模板,因此
[] (auto i) {
return i < 0;
}
class Z {
public:
Z();
template<typename T>
auto operator() (T i) const
{
return i < 0;
}
};
- 当闭包被调用时才会实例化成员函数模板,而不是出现lambda的位置。下面的lambda出现在main函数中,创建一个闭包,但直到把闭包和两个int传递给invoke(即invoke实例化时),闭包的函数调用符才被实例化
#include <iostream>
template<typename F, typename... Ts>
void invoke(F f, Ts... ps)
{
f(ps...);
}
int main()
{
::invoke([](auto x, auto y) {
std::cout << x + y << '\n';
},
21, 21);
}
别名模板(Alias Template)
- 无论带有模板实参的别名模板出现在何处,别名的定义都会被实参替代,产生的模式将用于推断
template<typename T, typename Cont>
class Stack;
template<typename T>
using DequeStack = Stack<T, std::deque<T>>;
template<typename T, typename Cont>
void f1(Stack<T, Cont>);
template<typename T>
void f2(DequeStack<T>);
template<typename T>
void f3(Stack<T, std::deque<T>);
void test(DequeStack<int> intStack)
{
f1(intStack);
f2(intStack);
f3(intStack);
}
template<typename T>
using A = T;
template<>
using A<int> = void;
Deduction Guide
- C++17引入了deduction guide,它用于将一个模板名称声明为一个类型标识符,通过Deduction guide不需要使用名称查找,而是使用模板实参推导,一个模板的所有deduction guide都会作为推导依据
explicitopt template-name (parameter-declaration-clause) -> simple-template-id;
- deduction guide有点像函数模板,但语法上有一些区别
- 看起来像尾置返回类型的部分不能写成传统的返回类型,这个类型(即上例的
S<T>
)就是guided type
- 尾置返回类型前没有auto关键字
- deduction guide的名称必须是之前在同一作用域声明的类模板的非受限名称
- guide的guided type必须是一个template-id,其template对应guide name
- 能被explicit限定符声明
- 使用deduction guide即可令特定实参类型推断为指定类型
template<typename T>
struct X {
T i;
};
X(const char*) -> X<std::string>;
int main()
{
X x{ "hello" };
std::cout << x.i;
}
template<typename T>
class X {
public:
X(T b) : a(b) {}
private:
T a;
};
template<typename T> X(T) -> X<T>;
int main()
{
X x{1};
X y(1);
auto z = X{12};
X xx(1), yy(2.0);
}
- 在
X x{1}
中的限定符X称为一个占位符类类型,其后必须紧跟一个变量名和初始化
X* p = &x;
- 使用花括号赋值可以解决没有初始化列表的问题,圆括号则不行
template<typename T>
struct X {
T val;
};
template<typename T> X(T)->X<T>;
int main()
{
X x1{42};
X x2 = {42};
X x3(42);
X x4 = 42;
}
- explicit声明的deduction guide只用于直接初始化
template<typename T, typename U>
struct X {
X(const T&);
X(T&&);
};
template<typename T> X(const T&) -> X<T, T&>;
template<typename T> explicit X(T&&) -> X<T, T>;
X x1 = 1;
X x2{2};
隐式的Deduction Guide
- C++17中的类模板实参推断本质是为类模板的每个构造函数和构造函数模板隐式添加了一个deduction guide
template<typename T>
class X {
public:
X(T b) : a(b) {}
private:
T a;
};
X x{12};
X y{x};
X z(x);
- 标准委员会有争议地决定两个都是
X<int>
类型,这个争议造成的问题如下
std::vector v{1, 2, 3};
std::vector v1{v};
std::vector v2{v, v};
template<typename T, typename... Ts>
auto f(T x, Ts... args)
{
std::vector v{ x, args... };
}
- 添加隐式deduction guide是有争议的,主要反对观点是这个特性自动将接口添加到已存在的库中。把上面的类模板定义修改如下,隐式deduction guide还会失效
template<typename T>
struct A {
using Type = T;
};
template<typename T>
class X {
public:
using ArgType = typename A<T>::Type;
X(ArgType b) : a(b) {}
private:
T a;
};
int main()
{
X x{1};
}
Deduction Guide的其他细微问题
template<typename T>
struct X {
template<typename U> X(U x);
template<typename U>
auto f(U x)
{
return X(x);
}
};
- 使用转发引用的deduction guide可能导致预期外的结果(推断出引用类型,导致实例化错误或产生空悬引用),标准委员会因此决定使用隐式deduction guide的推断时,禁用T&&这个特殊的推断规则
template<typename T>
struct X {
X(const T&);
X(T&&);
};
template<typename T> Y(const T&) -> Y<T>;
template<typename T> Y(T&&) -> Y<T>;
void f(std::string s)
{
X x = s;
}
template<typename ... Ts>
struct Tuple {
Tuple(Ts...);
Tuple(const Tuple<Ts...>&);
};
template<typename... Ts> Tuple(Ts...) -> Tuple<Ts...>;
template<typename... Ts> Tuple(const Tuple<Ts...>&) -> Tuple<Ts...>;
int main()
{
auto x = Tuple{1, 2};
Tuple a = x;
Tuple b(x);
Tuple c{x, x};
Tuple d{x};
auto e = Tuple{x};
}
- deduction guide不是函数模板,它们只用于推断而非调用,实参的传递方式对deduction guide声明不重要
template<typename T>
struct X {};
template<typename T>
struct Y {
Y(const X<T>&);
Y(X<T>&&);
};
template<typename T> Y(X<T>) -> Y<T>;