模板参数推导

函数模板类型推导

简单的函数模板:

1
2
template <typename T>
void f(ParamType param);

调用时:

1
f(expr);

在编译期间,编译器会推导两个类型:一个是 T ,另一个是 ParamType ,比如:

1
2
3
4
5
6
template <typename T>
void f(const T& param);

// 调用
int x = 0;
f(x);

T 推导为 int ,ParamType 推导为 const inst& ,T 的推导不仅取决于 expr 的类型,还取决于 ParamType 的类型。

ParamType 有三种情况:

  1. ParamType 是一个指针或引用但不是万能引用(&&)
  2. ParamType 是一个万能引用
  3. ParamType 既不是指针也不是引用

ParamType 是一个指针或引用但不是万能引用(&&)

推导规则:

  1. 如果 expr 的类型是一个引用,忽略引用部分。
  2. 然后剩下的部分决定 T ,然后 T 与形参匹配得出最终 ParamType 。

例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
template <typename T>
void f(T& param); // param 是一个引用

int x = 10; // x 是 int
const int cx = x; // cx 是 const int
const int& rx = cx; // rx 是指向 const int 的引用

f(x); // T 是 int ,ParamType 是 int&
f(cx); // T 是 const int ,ParamType 是 const int&
f(rx); // T 是 const int ,ParamType 是 const int&

指针同理

ParamType 是一个万能引用

推导规则:

  1. 如果 expr 是左值,T 和 ParamType 都会被推导为左值引用。
  2. 如果 expr 为右值,使用上面的推导规则。

例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
template <typename T>
void f(T&& param); // param 是万能引用类型

int x = 10; // x 是 int
const int cx = x; // cx 是 const int
const int& rx = cx; // rx 是指向 const int 的引用

f(x); // x 是左值,所以T 是 int& ,ParamType 也是 int&
f(cx); // cx 是左值,所以T 是 const int& ,ParamType 也是 const int&
f(rx); // rx 是左值,所以T 是 const int& ,ParamType 也是 const int&
f(10); // 10 是右值,所以 T 是 int ,ParamType 就是 int&&

ParamType 既不是指针也不是引用

推导规则:

  1. 如果 expr 是一个引用,忽略引用部分。
  2. 如果忽略引用后 expr 是一个 const 或 volatile ,那么再忽略之。

例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
template <typename T>
void f(T param); // param 是值类型

int x = 10; // x 是 int
const int cx = x; // cx 是 const int
const int& rx = cx; // rx 是指向 const int 的引用
const char* const ptr = "fun"; //  ptr 是一个常量指针,指向常量对象

f(x); // T 是 int ,ParamType 也是 int
f(cx); // T 是 int ,ParamType 也是 int
f(rx); // T 是 int ,ParamType 也是 int
f(ptr); // T 是 const char* , ParamType 也是 const char *

数组和函数实参

在很多上下文中,数组会退化为指向它一个元素的指针:

1
2
const char name[] = "Hello"; // name 的类型是 const char[6]
const char* ptrToName = name; // 数组退化为指针

将数组类型传值给模板,会推导出一个指针类型,因为函数的数组声明会被视为指针声明,即 void f(int param[]); 等价于 void f(int* param);

1
2
3
4
template <typename T>
void f(T param);

f(name); // T 是 const char*

但是函数可以接受指向数组的引用,所以如果 ParamType 为引用,T 会被推导为真正的数组类型。

1
2
3
4
template <typename T>
void f(T& param);

f(name); // T 是 const char[13] ,ParamType 是 const char(&)[13]

函数类型和数组类似,也会退化为一个函数指针。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
  void func(int, double); // func 类型是 void(int, double)

  template <typename T>
  void f1(T param);

template <typename T>
  void f2(T& param);

f1(func); // T 是 void(*)(int, double)
f2(func); // T 是 void(int, double) ,ParamType 是 void(&)(int, double)

std::initializer_list 实参

模板参数为 T 时,无法推导;为 std::initializer_list<T>T const(&)[N] 时才能推导:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
template <typename T>
void f1(T param);

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

template <typename T, int N>
void f3(T const(&param)[N]);

f1({1, 2, 3}); // error, 无法推导出 T
f2({1, 23}; // T 为 int ,ParamType 为 std::initializer_list<int>
f3({1, 2, 3}); // T 为 int ,ParamType 为 const int[3]

auto 自动类型推导

auto 类型推导和函数模板类型推导规则基本一致,除了 std::initializer_list 这个例外情况。

当一个变量使用 auto 进行声明时,auto 扮演模板的角色,变量的类型说明符扮演 ParamType 的角色,下面是理想化的推导过程:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
auto x = 27;

template <typename T>
void fx(T param);
fx(27); // 推导出 T 是 int ,ParamType 为 int ,即 x 为int 。

const auto cx = x;

template <typename T>
void fcx(const T param);
fcx(27); // 推导出 T 是 int ,ParamType 为 const int ,即 cx 为const int 。

const auto& rx = cx;
template <typename T>
void frx(const T& param);
frx(27); // 推导出 T 是 int ,ParamType 为 const int& ,即 cx 为const int& 。

auto 类型说明符的三种情况:

  • 类型说明符是一个个指针或引用但不是万能引用
  • 类型说明符是一个万能引用
  • 类型说明符既不是指针也不是引用
1
2
3
4
5
auto x = 10; // auto 为 int ,x 为 int
const auto& rx = x; // auto 为 int ,rx 为 const int&

auto&& lv = x; // auto 为 int& ,lv 为 int& && 折叠为 int&
auto&& rv = 10; // auto 为 int ,rv 为 int&&

数组和函数一样:

1
2
3
4
5
6
7
const char name[] = "hello";
auto arr1 = name; // arr1 是 const char*
auto& arr2 = name; // arr2 是 const char (&)[6];

void func(int, double);
auto func1 = func; // func1 是 void(int, double)
auto& func2 = func; // func2 是 void (&)(int, double)

auto 统一初始化推导

C++11 引入了统一初始化(uniform initialization)的语法:

  • 直接初始化: T object {arg1, arg2, ...};
  • 拷贝初始化: T object {arg1, arg2, ...};

在 C++17 之前,auto 统一初始化的类型都被推导为 std::initializer_list

1
2
3
4
auto a = {10};   // std::initializer_list<int>
auto b {10};  // std::initializer_list<int>
auto c = {1, 2};  // std::initializer_list<int>
auto d {1, 2};  // std::initializer_list<int>

C++17 改变了规则:

  • 拷贝初始化如果所有参数类型相同,推导为 std::initializer_list<T>
  • 直接初始化如果只有一个参数,推导为 T ,如果有多个参数,推导失败
1
2
3
4
auto a = {10};   // std::initializer_list<int>
auto b {10};  // int
auto c = {1, 2};  // std::initializer_list<int>
auto d {1, 2};  // c++ 17 错误,太多参数

注意: 在用比较新版本的编译器时,=auto d {1, 2}= 用 c++11 编译也会出错,应该是编译器做了限制,gcc 用 -fpermissive 选项可以编译成功。

C++14 允许 auto 用于函数返回值和 lambda 函数的形参,使用的机制是模板类型推导,所以无法从直接初始化列表推导。

1
2
3
4
5
6
7
auto func() {
  return {1, 2, 3}; // 错误,推导失败
}

std::vector<int> v;
auto reset = [&v](const auto& newV) { v = newV; };
reset({1, 2, 3}); // 错误,推导失败

auto 的优点

  • 使用 auto 声明变量必须初始化,能够避免使用未初始化的变量
    1
    2
    3
    
    int x;    // 未初始化的变量
    auto x2; // 错误,必须初始化
    auto x3 = 0
    
  • auto 能够表示只有编译器才知道的类型,比如声明 lambda 函数
    1
    2
    3
    4
    5
    6
    7
    
    auto f = [const auto& p1, const auto& p2] { return *p1 < *p2; };
    
    
    // 没有 auto 的写法,用 std::function
    std::function<bool(const std::unique_ptr<Widget> &p1,const std::unique_ptr<Widget> &p2)>
    dereUPLess =
      [](const std::unique_ptr<Widget> &p1,const std::unique_ptr<Widget> &p2){return *p1<*p2;};
    
  • auto 可以解决依赖机器的类型问题
    1
    2
    3
    4
    
    std::vector<int> v;
    auto sz = v.size(); // size 在 32 bit 和 64 bit 上大小不同
    
    std::vector<int>::size_type sz = v.size(); // 原来的写法
    
  • auto 可以避免意识不到的类型不匹配错误
    1
    2
    3
    4
    5
    6
    7
    
    std::unordered_map<std::string, int>;
    
    // 错误,std::unordered_map 的 key 是一个常量,std::pair 的类型为 std::pair<const std::string, int>
    for (const std::pair<std::string, int>& p: m) {}
    
    // auto 可以避免这些问题
    for (const auto& p: m) {}
    

auto 的陷阱

auto 有时推导出的类型并不是我们需要的类型,常见于使用代理类并具有隐式类型转换

1
2
3
4
5
std::vector<bool> features(const Widget& w); // bool 表示 Widget 是否有对应的 feature

// 是否具有高优先级
bool highPriority = features(w)[5];
processWidget(w, highPriority);

如果使用 auto ,就会导致未定义行为

1
2
auto highPriority = features(w)[5];
processWidget(w, highPriority);

原因是 std::vector<bool> 的 operator[] 返回一个代理类 std::vector<bool>::reference 来扮演 bool& ,同时它能够隐式转换成 bool 。使用 auto 导致 highPriority 的类型为 std::vector<bool>::reference ,它没有第五 bit 的值,所以这个值取决于 std::vector<bool>::reference 的具体实现。

解决方法是直接使用 bool 或者使用 auto 但是进行一次显示转换

1
auto highPriority = static_case<bool>(features(w)[5]);

使用 decltype

decltype 返回名字或者表达式的类型。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const i = 0; // decltype(i) 是 const int

bool f(const Widget& w);
// decltype(w) 是 const Widget&
// decltype(f) 是 bool(const Widget&)

struct Point {
  int x;
  int y;
}
// decltype(Point::x) 是 int
// decltype(Point::y) 是 int

decltype 主要用于函数模板返回类型,而这个返回类型依赖形参

1
2
3
4
5
template<typename Container,typename Index>
auto authAndAccess(Container&& c,Index i)->decltype(std::forward<Container>(c)[i]) {
  authenticateUser();
  return std::forward<Container>(c)[i];
}

C++14 支持 decltype(auto) ,上面可以优化一下

1
2
3
4
5
template<typename Container,typename Index>
decltype(auto) authAndAccess(Container&& c,Index i) {
  authenticateUser();
  return std::forward<Container>(c)[i];
}

对于一个名字,decltype 将会产生这个名字被声明的类型。如果一个左值表达式除了名字还有类型, decltype 会产生 T& 。

1
int x = 0;

decltype (x) 是 int ,但是 decltype((x)) 是 int& ,因为 (x) 是一个比名字更复杂的左值表达式。

使用 decltype(auto) 时,加上括号就会导致类型不同。

1
2
3
4
5
6
7
8
9
decltype(auto) f1() {
  int x = 0;
  return x; // decltype(x) 是 int ,f1 返回 int
}

decltype(auto) f2() {
  int x = 0;
  return (x); // decltype((x)) 是 int& ,f2 返回 int&
}

f2 返回了局部变量的引用,产生为定义行为。所以使用 decltype(auto) 时一定要小心,确保类型推导是想要的结果。