Zach的博客

Type deduction in CPP

模板类型推导

假设我们有如下的模板函数:

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

一个该模板函数的调用语句可能如下所示:

1
f(expr);

在编译阶段,编译器利用expr来推导出两个类型:TParamTypeParamType常带有变量的限定符,如const和引用符号。
模板的类型推到不仅仅依赖于expr的类型,还依赖于ParamType的类型,ParamType共分为三种类型:

  1. ParamType是一个指针或者引用(但不是universal reference, T&&)
  2. ParamType是一个universal reference
  3. ParamType既不是一个指针也不是一个引用
    根据ParamType的类型,模板的类型推到规则也有三种。

1. ParamType是一个指针或者引用(但不是一个universal reference)

这种情况下的推导规则为:

  1. 如果expr是一个引用,那么忽略expr的引用
  2. 然后根据expr的类型和ParamType的类型,推导出T

举几个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template <typename T>
void f(T& param);

int x = 27;
const int cx = x; // cx is const int
const int& rx = x; // rx is const int&

f(x); // x is int => T is int, param's type is int&
f(cx); // cx is const int => T is const int, param's is const int&
f(rx); // rx is const int& => T is const int, param's type is const int&

template <typename T>
void f(T* param);

int x = 27;
const int *px = &x;

f(&x); // T is int, param's type is int*
f(px); // T is const int, param's type is const int*

2. ParamType是一个universal reference

一般来说函数模板有以下形式:

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

param类型由以下规则得得出:

  1. T& + T& => T&
  2. T& + T&& => T&
  3. T&& + T& => T&
  4. T&& + T&& => T&&

举几个例子:

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

int x = 27;
const int cx = x;
const int& rx = x;

f(x); // x is lvalue, T is int&, param's type is int&
f(cx); // cx is lvalue, T is const int&. param's type is const int&
f(rx); // rx is lval, T and param is const int&
f(27); // 27 is rvalue, T is int, param's type is int&&

3. ParamType既不是指针也不是引用

一般来说函数模板有以下形式:

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

此时,传递给函数的实参会被拷贝。此时函数模板的推导规则如下:

  1. 如果expr是引用,那么它的引用被忽略
  2. 之后,如果exprconst限定符,那么const被忽略,如果有volatile限定符,volatile被忽略。
    还是例子:
    1
    2
    3
    4
    5
    6
    7
    int x = 27;
    const int cx = x;
    const int& rx = x;

    f(x); // T's and param's types are both int
    f(cx); // T's and param's types are both int
    f(rx); // ditto

注意到只是顶级const被忽略,如果有这样一次函数调用:

1
2
const char* const ptr = "Fun with pointers";
f(ptr);

那么在函数中修改ptr所指向的字符串是不被允许的,而修改ptr这个指针(如使其为nullptr)则是可以的。

数组参数

数组和指针是不同的类型,但是数组类型自动转换成指针类型,于是下面的代码是成立的:

1
2
const char name[] = "Test";
const char* ptrName = name;

在模板函数中,如果数组是有by-value传递给函数的,那么数组自动转换成指针,如果by-reference传递给函数,那么函数得到的是一个数组的引用。即:

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

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

int arr[] = {1, 2, 3};

f1(arr); // T is int*
f2(arr); // T is int[], parameter's type is int (&)[]

于是我们可以用以下函数在编译期得到一个数组的大小:

1
2
3
4
template <typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept {
return N;
}

当然在C++11标准中,更推荐用std::array来使用数组

1
2
int keyVals[] = {1, 2, 3};
std::array<int, arraySize(keyVals)> anotherArray;

函数参数

函数在C++中也是一种类型,而函数指针是一个指针类型,只是函数类型可以自动转换成函数指针。在函数模板中,函数的转换规则和数组类型相同。
举几个例子:

1
2
3
4
5
6
7
8
9
10
void someFunc(int, double);

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

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

f1(someFunc); // param's type is void (*)(int, double)
f2(someFunc); // param's type is void (&)(int, double)

auto类型推导

auto关键字是由C++11引入的,本质上auto类型推导和模板类型推导是一样的,只要把auto当作是T,而变量的类型当作是模板参数类型,等号右侧当作是expr即可。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
auto x = 27;        // auto -> T, auto -> paramType, so x is int
const auto cx = 27; // auto -> T, const auto -> paramType, so T is int, cx is const int
const auto& rx = x; // auto -> T, const auto& -> paramType, so T is int, rx is const int&

auto&& uref1 = x; // x is lvalue => uref1 is int&
auto&& uref2 = cx; // cx is lvalue => uref2 is const int&
auto&& uref3 = 28; // 28 is rvalue => uref3 is int&&

const char name[] = "Test";

auto arr1 = name; // arr1 is const char*
auto& arr2 = name; // arr2 is const char (&)[5]

void someFunc(int, double);
auto func1 = someFunc; // func1 is void (*)(int, double)
auto& func2 = someFunc; // func2 is void (&)(int, double)

唯一的不同是,对于auto,如果等号右边是大括号包围的列表,那么变量自动推导为std::initializer_list<T>,而对于模板函数,传递一个{t1, t2, ..., tn}给函数会导致编译错误。

1
2
3
4
5
6
7
8
9
10
11
auto t = {1, 2, 3};     // t's type is std::initializer_list<int>

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

f({1, 2, 3}); // error!

template <typename T>
void f_(std::initializer<T> il);

f_({1, 2, 3}); // correct

decltype类型推导

decltype关键字用来得到表达式的类型。给几个例子:

1
2
3
4
5
6
7
8
9
10
11
const int i = 0;    // decltype(i) is const int
bool f(const Widget& w); // decltype(f) is bool(const Widget&), function type
// decltype(w) is const Widget&
struct Point {
int x, y;
}; // decltype(Point::x) is int
// decltype(Point::y) is int
Widget w; // decltype(w) is Widget
if (f(w)) {...} // decltype(f(w)) is bool or can convert to bool

std::vector<int> v; // decltype(v) is std::vector<int>

于是我们可以写出以下函数:

1
2
3
4
5
6
template <typename T, typename Index>
auto authAndAccess(Container& c, Index i) -> decltype(c[i])
{
authUser();
return c[i];
}

operator[]可能返回容器中的一个元素的引用或者返回容器中元素的拷贝,通过上述函数我们可以将这两种情况一同处理。
decltype有一个特殊情况需要注意,如果括号内是一个变量名,那么通常我们得到的是我们想要的类型,如果括号内是一个表达式,那么我们得到的是一个T&,即

1
2
3
int x = 0;
// decltype(x) is int
// decltype((x)) is int&

boost::typeindex

boost中的typeindex模块可以在运行时得到正确的推导类型,一个例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <vector>

#include <boost/type_index.hpp>

struct Widget {
int x;
};

template <typename T>
void f(const T& param) {
using std::cout;
using boost::typeindex::type_id_with_cvr;

cout << "T =\t"
<< type_id_with_cvr<T>().pretty_name()
<< '\n';

cout << "param =\t"
<< type_id_with_cvr<decltype(param)>().pretty_name()
<< '\n';
}

int main() {
std::vector<Widget> vw = {{1}, {2}, {3}};
const auto k = vw;
f(&k[0]);
return 0;
}

输出

1
T =     Widget const*
param =     Widget const* const&