14-模板实参推断

推断的过程

  • 每个实参-参数对的推断都是独立的,如果结果矛盾推断就会失败
template<typename T>
const T& max(const T& a, const T& b)
{
  return a < b ? b : a;
}
auto g = max(1, 1.0); // T分别推断int和double,推断矛盾
  • 即使所有推断不发生矛盾,也可能推断失败
template<typename T>
typename T::E at(T a, int i)
{
  return a[i];
}

void f(int* p)
{
  int x = at(p, 7); // T为int*,T::E构造无效,推断失败
}
  • 对于字符串类型实参会出现意想不到的错误
template<typename T>
const T& max(const T& a, const T& b);

max("Apple", "Pear"); // 错误:分别是const char[6]和const char[5]

推断上下文(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); // T为int**
  f2(b); // T = bool,N = 42
  f3(&S::f); // T1 = void,T2 = S,T3 = double
}
  • 复杂的类型声明的匹配过程从最顶层构造开始,然后不断递归子构造,即各种组成元素,这些构造被称为推断上下文。下列构造不能作为推断上下文用来推断模板参数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> // X<N>::I是非推断上下文,但X<N>::*p是推断上下文
void f(void (X<N>::*p)(typename X<N>::I));

int main()
{
  f(&X<33>::f); // 正确:由X<N>::*p获得N,再放入X<N>::I,N被推断为33
}
  • 相反,参数类型完全依赖于推断上下文也可能导致推断矛盾
template<typename T>
void f(X<Y<T>, Y<T>>);

void g()
{
  f(X<Y<int>, Y<int>>()); // OK
  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); // S要转为int(&)[20],T推断为int[20]
}

初始化列表(Initializer List)的推断

  • 函数调用的实参是一个初始化列表时,实参没有具体类型,不会发生推断
#include <initializer_list>

template<typename T>
void f(T);

int main()
{
  f({1, 2, 3}); // 错误:不能由initializer_list推断T
}
  • 但可以这样推断
#include <initializer_list>

template<typename T>
void f(std::initializer_list<T>);

int main()
{
  f({2, 3, 5, 7, 9}); // OK:T推断为int
  f({'a', 'e', 'i', 'o', 'u', 42}); // 错误:T推断为char和int
}

参数包的推断

  • 一个参数包能匹配多个实参
template<typename First, typename... Rest>
void f(First first, Rest... rest);

void g(int i, double j, int* k)
{
  f(i, j, k); // First推断为int,Rest推断为{double, int*}
}
  • 多个模板参数的值和参数包能由每个实参类型决定
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); // OK:T推断为int,Ts推断为{float, double}
  g(x1, x2); // OK:Ts1推断为{int, int},Ts2推断为{float, double}
  f(x1, x3); // 错误:T由第一个实参推断为int,由第二个推断为double
  g(x1, x3); // OK:Ts1推断为{int, double},Ts2推断为{float, double}
}
  • 参数包推断不仅限于函数参数包
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); // OK: Ts推断为{short, int}
  g(x1, x1); // OK: Ts1推断为{short, int},Ts2推断为{short, int}
  f(x1, x2); // 错误:Ts推断为{short, int}和{unsigned short, unsigned}
  g(x1, x2); // OK:Ts1推断为{short, int},Ts2推断为{unsigned short, unsigned}
}

字面值运算符模板(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; // 模板实参列表为<'3', '.', '1', '4'>,打印'3' '.' '1' '4'
  • 这个技术只支持即使没有后缀(suffix)也有效的数字字面值
auto b = 01.3_B7; // OK:推断为<'0', '1', '.', '3'>
auto c = 0xFF00_B7; // OK:推断为<'0', 'x', 'F', 'F', '0', '0'>
auto d = 0815_B7; // 错误:8不是有效的八进制数字
auto e = hello_B7; // 错误:hello不是有效的字面值
auto f = "hello"_B7; // 错误:不匹配模板

引用折叠(Reference Collapsing)

& + &&
& + &&&
&& + &&
&& + &&&&
  • 顶层的cv限定符会被丢弃
using RCI = const int&; // 底层类型
volatile RCI&& r = 42; // OK: r为const int&
using RRI = int&&; // 底层类型
const RRI&& rr = 42; // OK: rr为int&&

转发引用(Forwarding Reference)

  • 当函数参数是一个转发引用时,模板实参推断不仅考虑函数调用实参类型,还会考虑实参是左值还是右值
    • 实参为左值则参数类型推断为实参类型的左值引用,引用折叠规则将确保替换的参数是一个左值引用
    • 实参为右值则参数类型推断为实参类型(不是引用类型),替换的参数是一个此类型的右值引用
template<typename T>
void f(T&& p);

void g()
{
  int i;
  const int j = 0;
  f(i); // 实参为左值,T推断为int&,参数类型为int&
  f(j); // 实参为左值,T推断为const int&,参数类型为const int&
  f(2); // 实参为右值,T推断为int,参数类型为int&&
}
  • 这隐藏着一个问题,若T为引用类型,模板定义中涉及未初始化的T类型就会出错
template<typename T>
void f(T&&)
{
  T x; // 传递左值时,T推断为引用,x未初始化所以出错
}
template<typename T>
void f(T&&)
{
  std::remove_reference_t<T> x; // x一定不会是一个引用
}

完美转发

  • 引用折叠的的规则使得函数模板可以接受几乎任何实参并捕获它的关键属性(类型以及是左值还是右值),由此函数模板能转发实参给另一个函数,这种技术称为完美转发
template<typename T>
void f(T&& x)
{
  g(std::forward<T>(x)); // 把x完美转发给g
}
  • 完美转发也可以结合可变参数模板把任意数量实参转发给另一个函数
template<typename... Ts>
void f(Ts&&... args)
{
  g(std::forward<Ts>(args)...); // 把所有args转发给g
}
  • 完美转发处理空指针常量时会造成问题,整型值会被当作常量值0
void g(int*);
void g(...);

template<typename T>
void f (T&& x)
{
  g(std::forward<T>(x));
}

void foo()
{
  g(0); // 调用g(int*)
    f(0); // 调用g(...)
}
  • 这也是使用nullptr替代空指针的原因
g(nullptr); // 调用g(int*)
f(nullptr); // 调用g(int*)
  • 完美转发调用另一个函数的返回类型时,可以用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)...);
}

SFINAE(Substitution Failure Is Not An Error)

  • SFINAE用来禁止不相关函数模板在重载解析时造成错误
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); // OK:只匹配第二个,因为第一个替换失败
  ::begin(a); // OK:只匹配第一个,因为第二个替换失败
}
  • 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); // 错误:在第一个模板中用int&替换T实例化Array<int&>
}
  • 使用auto返回类型,必须实例化定义来确定返回类型,实例化定义不属于即时上下文,此期间产生错误不会使用SFINAE
template<typename T>
auto f(T x)
{
  return x->m; // 实例化此定义时,x为int则错误
}

int f(...); // 省略号参数的匹配不好

template<typename T>
auto g(T x) -> decltype(f(x));

int main()
{
  g(42); // 错误:第一个匹配更好,但实例化定义时出错,不会使用SFINAE
}

推断的限制

  • 被替换的参数类型可以是实参类型的基类
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之前错误:不能从构造函数实参推断类模板参数T
  • 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"); // OK:T1 = int,T2 = double,T3 = const char*
C c2(1, 3.14); // OK:T1 = int,T2 = T3 = double
C c3("hi", "guy"); // OK:T1 = T2 = T3 = const char*
C c4; // 错误:T1和T2未定义
C c5("hi"); // 错误:T2未定义
// 注意不能显式指定一部分而推断另一部分
C<string> c10("hi","my", 42); // 错误:只指定了T1,T2未推断
C<> c11(1, 3.14, 2); // 错误:T1和T2都没指定
C<string, string> c12("hi","my"); // OK
  • 默认实参不能用于推断
template<typename T>
void f(T x = 42)
{}

int main()
{
  f<int>(); // OK: T = int
  f(); // 错误:不能用默认实参推断
}
template<typename T>
void f(T, int) noexcept(nonexistent(T()));

template<typename T>
void f(T, ...); // C-style vararg function

void test(int i)
{
  f(i, i); // 错误:选择第一个模板,但表达式nonexistent(T())无效
}
  • 同样的规则用于列出潜在的异常类型
template<typename T>
void g(T, int) throw(typename T::Nonexistent); // C++11弃用了throw

template<typename T>
void g(T, ...);

void test(int i)
{
  g(i, i); // 错误:选择第一个模板,但类型T::Nonexistent无效
}

显式指定实参

  • 指定空模板实参列表可以确保匹配模板
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(); // OK:在N中找不到f,声明一个新的f
  friend int f<>(); // 错误:查找到全局的模板f,返回类型不一致
};
}
  • 显式指定模板实参替换会使用SFINAE
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); // OK:1和2转换为double
g<double, int>(1, 2, 3);  // OK:模板实参是<double, int, int>

auto

  • auto使用了模板实参推断的机制
template<typename C>
void f(const C& c)
{
  auto it = c.begin();
  while (it != c.end())
  {
    auto& x = *it++;// 对元素进行操作
  }
}

// auto it = c.begin()的推断等价于如下调用模板的推断
template<typename T>
void f2(T it);

f2(c.begin());

// auto& x = *it++的推断等价于如下调用模板的推断
template<typename T>
void f3(T& x);

f3(*it++);
  • auto&&实际是转发引用
auto&& x = ...;
// 等价于
template<typename T>
void f(T&& x); // auto被T替换

int i;
auto&& rr = 42; // auto推断为int,int&& rr = 42
auto&& lr = i; // auto推断为int&,int& lr = i
  • auto&&常用于绑定一个值类型(左值或右值)未知的对象
template<typename C>
void f(C c)
{
  for (auto&& x: c) ...
}
  • C++14允许auto为函数返回类型
auto f() { return 42; }
// 也能用尾置返回类型
auto f() -> auto { return 42; }
// 第一个auto声明尾置返回类型
// 第二个auto是用于推断的占位符类型
  • 如果没有显式声明lambda的返回类型,则lambda的返回类型默认被视为auto
[] (int x) { return f(x); };
// 等价于
[] (int x) -> auto { return f(x); };
  • 返回类型为auto的函数也能分开声明和定义
auto f(); // 前置声明
auto f() { return 42; } // OK

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;
  • C++17允许由模板实参推断
template<auto N>
struct X;

X<42>* p; // auto推断为int
X<3.14>* q; // 错误:非类型模板参数不能是浮点数
  • 在定义中可以用decltype表示对应实参的类型
template<auto N>
struct X {
  using Type = decltype(N);
};
  • auto非类型模板参数还可以用于参数化类成员模板
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(); // 增加s.i
}
  • 使用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; // y1和x类型不一定相同,如果x是char,y1是int
decltype(x) y2 = x + 1; // y2和x类型一定相同
  • 反之,如果e是其他表达式,decltype(e)产生一个反映表达式type或value的类型
    • e是T类型lvalue,产生T&
    • e是T类型xvalue,产生T&&
    • e是T类型prvalue,产生T
int&& i = 0;
decltype(i) // int&&(i的实体类型)
decltype((i)) // int&(i的值类型)

decltype(auto)

int i = 42;
const int& r = i;
auto x = r; // int x = r
decltype(auto) y = r; // const int& y = r

std::vector<int> v = { 42 };
auto x2 = v[0]; // int x2 = v[0],x2是一个新对象
decltype(auto) y2 = v[0]; // int& y2 = v[0],y2是一个引用
template<typename C>
class X {
  C c;
  decltype(auto) operator[](std::size_t n)
  {
    return c[n];
  }
};
  • 圆括号初始化会产生和decltype一样的影响
int i;
decltype(auto) x = i; // int x = i
decltype(auto) r = (i); // int& 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;   // produces S<42>
S<(v)> sv; // produces S<(int&)v>
  • 在函数模板中使用可推断的非类型参数
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); // OK
int r2 = deduce(42); // 错误:decltype(V)是一个非推断上下文

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 }); // OK:T推断为int
  • 对初始化列表的推断,auto会将其视为std::initializer_list,即上述第一种情形
auto x = { 1, 2 }; // x是initializer_list<int>
  • C++14禁止了对auto用initializer_list直接初始化,必须用=
auto x  { 1, 2 }; // 错误
  • 但允许单个元素的直接初始化,这和圆括号初始化一样,不会被推断为std::initializer_list
auto x { 1 }; // x为int,和auto x(1)效果一样
  • 返回类型为auto时不能返回一个初始化列表
auto f() { return { 1 }; } // 错误
  • 共享auto的变量必须有相同推断类型
char c;
auto *p = &c, d = c; // OK
auto e = c, f = c + 1; // 错误:e为char,f为int

auto f(bool b)
{
  if (b)
  {
    return 42.0; // 返回类型推断为double
  }
  else
  {
    return 0; // 错误:推断不一致
  }
}
  • 如果返回的表达式递归调用函数,则不会发生推断从而导致出错
// 错误例子
auto f(int n)
{
  if (n > 1)
  {
    return n * f(n - 1); // 错误:f(n - 1)类型未知
  }
  else
  {
    return 1;
  }
}

// 正确例子
auto f(int n)
{
  if (n <= 1)
  {
    return 1; // OK:返回类型被推断为int
  }
  else
  {
    return n * f(n - 1); // OK:f(n - 1)为int,所以n * f(n - 1)也为int
  }
}
  • auto返回类型没有对应的副本时会推断为void,若不能匹配void则出错
auto f1() {} // OK:返回类型是void
auto f2() { return; } // OK:返回类型是void
auto* f3() {} // 错误:auto*不能推断为void
  • 当考虑到SFINAE会有一些意料外的情况
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)  // 必须实例化t和u来确定返回类型
{
  return t + u;  // 此处的实例化在定义中,不是即时上下文,不适用SFINAE
}

void g(...);

struct X {};

using A = decltype(f(X(), X())); // OK:A为void
using B = decltype(g(X(), X())); // 错误:g<X, X>的实例化非法

结构化绑定(Structured Binding)

  • 结构化绑定是C++17引入的新特性,作用是在一次声明中引入多个变量。一个结构化绑定必须总有一个auto,可以用cv限定符或&、&&声明符
struct X { bool valid; int value; };
X g();
const auto&& [b, N] = g(); // 把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];  // f()返回一个int数组的引用

auto [ x, y ] = f(); // auto e = f(), x = e[0], y = e[1]
auto& [ r, s ] = f(); // auto& e = f(), x = e[0], y = e[1]

// std::tuple的例子
std::tuple<bool, int> bi {true, 42};
auto [b, i] = bi; // auto b = get<0>(bi), i = get<1>(bi)
int r = i; // int r = 42
  • 对于tuple-like类E(或它的表达式,设为e),如果std::tuple_size<E>::value是一个有效的整型常量表达式,它必须等于中括号标识符的数量。如果表达式e有名为get的成员,表现如下
std::tuple_element<i, E>::type& n_i = e.get<i>();
// 如果e推断为引用类型
std::tuple_element<i, E>::type&& n_i = e.get<i>();

// 如果e没有get成员
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; // 将M映射为一对值
};

template<>
struct std::tuple_element<0, M> {
  using type = int; // 第一个值类型为int
};

template<>
struct std::tuple_element<1, M> {
  using type = double; // 第二个值类型为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(); // 相当于int&& i = 42, double&& d = 7.0

泛型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;
  });
}
  • C++14中lambda的参数类型可以为auto
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
// 上述的lambda相当于下面类的一个默认构造对象的简写
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>); // 等价于f2

void test(DequeStack<int> intStack)
{
  f1(intStack); // OK:T推断为int,Cont推断为std::deque<int>
  f2(intStack); // OK:T推断为int
  f3(intStack); // OK:T推断为int
}
  • 别名模板不能被特化
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" }; // T推断为std::string
  std::cout << x.i;
}
  • 将deduction guide用于类模板推断
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<int> x{1}
  X y(1); // X<int> y(1)
  auto z = X{12}; // auto z = X<int>{1}
  X xx(1), yy(2.0); // 错误:X推断为X<int>和X<double>
}
  • 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}; // OK
  X x2 = {42}; // OK
  X x3(42); // 错误:没有初始化列表,int不能转为X<int>
  X x4 = 42; // 错误:没有初始化列表,int不能转为X<int>
}
  • 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; // 只使用非explicit声明的deduction guide:X<int, int&> x1 = 1
X x2{2}; // 第二个deduction guide更合适:X<int, int> x2{2}

隐式的Deduction Guide

  • C++17中的类模板实参推断本质是为类模板的每个构造函数和构造函数模板隐式添加了一个deduction guide
template<typename T>
class X {
 public:
  X(T b) : a(b) {}
 private:
  T a;
};

// template<typename T> X(T) -> X<T> // 隐式deduction guide
  • deduction guide有一个歧义
X x{12}; // x类型为X<int>
X y{x}; // y类型为X<int>还是X<X<int>>?
X z(x); // z类型为X<int>还是X<X<int>>?
  • 标准委员会有争议地决定两个都是X<int>类型,这个争议造成的问题如下
std::vector v{1, 2, 3};
std::vector v1{v}; // vector<int>
std::vector v2{v, v}; // vector<vector<int>>
  • 编程中很容易错过这种微妙之处
template<typename T, typename... Ts>
auto f(T x, Ts... args)
{ // 如果T推断为vector
  std::vector v{ x, args... };  // 参数包是否为空将决定不同的v类型
}
  • 添加隐式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;
};

// template<typename T> X(typename A<T>::Type) -> X<T>; // 隐式deduction guide
// 该deduction guide无效,因为有限定名称符A<T>::
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); // 根据注入类名规则X是X<T>,根据类模板实参推断X是X<U>
  }
};
  • 使用转发引用的deduction guide可能导致预期外的结果(推断出引用类型,导致实例化错误或产生空悬引用),标准委员会因此决定使用隐式deduction guide的推断时,禁用T&&这个特殊的推断规则
template<typename T>
struct X {
  X(const T&);
  X(T&&);
};

// 如果把隐式deduction guide指定出来就将出错
template<typename T> Y(const T&) -> Y<T>; // (1)
template<typename T> Y(T&&) -> Y<T>; // (2)

void f(std::string s)
{
  X x = s; // 预期想通过隐式deduction guide推断T为std::string
  // (1)推断T为std::string,但要求实参转为const std::string
  // (2)推断T为std::string&,是一个更好的匹配,这是预期外的结果
}
  • 拷贝构造和初始化列表的问题
template<typename ... Ts>
struct Tuple {
  Tuple(Ts...);
  Tuple(const Tuple<Ts...>&);
};

// 隐式deduction guide
template<typename... Ts> Tuple(Ts...) -> Tuple<Ts...>; // (1)
template<typename... Ts> Tuple(const Tuple<Ts...>&) -> Tuple<Ts...>; // (2)

int main()
{
  auto x = Tuple{1, 2}; // 明显使用(1),x是Tuple<int, int>
  Tuple a = x; // (1)为Tuple<Tuple<int, int>,(2)为Tuple<int, int>,(2)匹配更好
  Tuple b(x); // 和a一样推断为Tuple<int, int>,a和b都由x拷贝构造
  Tuple c{x, x}; // 只能匹配(1),生成Tuple<Tuple<int, int>, Tuple<int, int>>
  Tuple d{x}; // 看起来和c的匹配一样,但会被视为拷贝构造,匹配(2)
  auto e = Tuple{x}; // 和d一样,推断为Tuple<int, int>而非<Tuple<int, int>>
}
  • 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>; // 虽然不对应构造函数但没有关系
// 对于一个X<T>类型的值x将选用可推断类型Y<T>
// 随后初始化并对Y<T>的构造函数进行重载解析
// 根据x是左值还是右值决定调用的构造函数

文章作者: 张小飞
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC-ND 4.0 许可协议。转载请注明来源 张小飞 !
 上一篇
15-特化与重载 15-特化与重载
重载模板以适用不同的情况 下面用于交换两个对象的函数模板exchange可以处理简单类型,但如果T是下面的类,就没必要再拷贝一次对象并调用两次赋值运算符,而只需要使用成员模板exchangeWith交换内部的指针 template<
下一篇 
13-实例化 13-实例化
On-Demand 实例化(隐式实例化) 编译器遇到模板特化时会用所给的实参替换对应的模板参数,从而产生特化 如果声明一个指向某类型的指针或引用,不需要看到类模板定义,但如果要访问特化的成员或想知道模板特化的大小,就要先看到模板定义 te
  目录