Effective Modern C++ 笔记 - 类型推断

C++98有一组用于类型推断的规则: 用于函数模板的规则。C++11对这个规则集进行了一些修改,并添加了两个新的规则集,一个用于auto,另一个用于decltype。然后C++14扩展了autodecltype可能被使用的上下文。类型推断的使用场合越来越广,这使您不再需要手写那些明显或冗余的类型。它使C++代码的适应性更强,因为在源代码中的某一点更改类型会自动通过类型推断传播到其他位置。但是,它会使代码更难于推理,因为编译器推导出的类型可能和您希望的不同。

如果对于类型推导操作没有一个扎实的理解,要想写出有现代感的C++程序是不可能的。类型推导随处可见:在函数模板调用中,在auto出现的地方,在decltype表达式出现的地方,以及C++14decltype(auto)中。

1. 理解模板类型推断

假设有下面的代码:

1
2
3
int x=27;
const int cx = x;
const int& rx = x;

情形一: 参数是引用或指针

1
2
3
4
template<typename T> void f(T& param);
f(x); // param是 int&, T是int
f(cx); // param是 const int&, T是const int
f(rx); // param是 const int&, T是const int
1
2
3
4
template<typename T> void f(const T& param);
f(x); // param是 const int&, T是int
f(cx); // param是 const int&, T是int
f(rx); // param是 const int&, T是int
1
2
3
4
5
template<typename T> void f(T* param);
const int* px = &x;

f(&x); // 参数是int*, T是int
f(px); // 参数是const int*, T是const int

情形二: 参数是通用引用(Universal Reference)

普通函数上的&&是通用引用,既支持左值引用也支持右值引用.

1
2
3
4
5
template<typename T> void f(T&& param);
f(x); // 参数是 int&, T是int&
f(cx); // 参数是 const int&, T是const int&
f(rx); // 参数是 const int&, T是const int&
f(27); // 参数是 int&&, T是int, <--注意这里, 靠它完美转发

情形三: 参数既不是指针也不是引用

1
2
3
4
template<typename T> void f(T param);
f(x); // 参数是 int
f(cx); // 参数还是 int
f(rx); // 参数仍然是 int

数组类型的参数

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

const char* const ptr = "hello world";
const char name[] = "hello world";

f1(ptr); // 参数是const char*, 指针本身的const被去除
f1(name); // 参数还是const char*
f2(name); // 这时参数是 const char(&)[12]

我们可以定义一个取数据数量的函数

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

函数类型的参数

1
2
3
4
5
6
7
template<typename T> void f1(T param);
template<typename T> void f2(T& param);

void someFunc(int, double);

f1(someFunc); // 参数是函数指针 void(*)(int, double)
f2(someFunc); // 参数是函数引用 void(&)(int, double)

2. auto 推断

假设有下面的代码:

1
2
3
4
5
int x=27;
const int cx = x;
const int& rx = x;
const char name[] = "hello world";
void someFunc(int, double);
1
2
3
4
5
6
7
8
9
10
auto&& uref1 = x;   // int&
auto&& uref2 = cx; // const int&
auto&& uref3 = rx; // const int&
auto&& uref4 = 27; // int&&

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

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

注意小括号和大括号的区别

1
2
3
4
5
6
7
8
9
10
auto x1=27;   // int
auto x2(27); // int
auto x3={27}; // std::initialize_list<int>
auto x4{27}; // 存疑, VS测试结果是int, GCC结果是std::initialize_list<int>

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

f1({11,23,9}); // 错误
f2({11,23,9}); // OK

3. decltype 推断

1
2
3
4
5
6
7
8
// decltype 推断类型
const int i = 0; // decltype(i) 是 const int
std::vector<int> v; // decltype(v[0]) 是 int&

// decltype(auto) 和 auto类似, 但使用decltype的推断方式
const Widget& cw = w;
auto myWidget1 = cw; // 这是Widget
decltype(auto) myWidget2 = cw; //这是const Widget&

auto推断会把引用去除, 所以下面auto作为返回值时不会返回int&

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template <typename C, typename I>
auto getItem(C &c, const I &i)
{
return c[i];
}

// 使用decltype(auto)作为返回值时就可以得到 int& 了
template <typename C, typename I>
decltype(auto) getItem2(C &c, const I &i) // 注意这个函数有巨坑
{
return c[i];
}

std::vector<int> v = {1,2,3};
getItem(v, 2) = 10; // 错
getItem2(v, 2) = 10; // 对

巨坑

注意上面的getItem2函数, 当参数c是右值时, 会产生一个临时对象, 这时返回的是已经析构的引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template <typename C, typename I>
decltype(auto) getItem2(C &c, const I &i)
{
return c[i];
}

std::vector<Widget> getVec()
{
return std::vector<Widget>(10);
}

int main(int, char**) {
Widget& w = getItem2(getVec(), 5);
w.show(); // 这个w已经析构了
return 0;
}

正确的版本是要能处理右值, 所以应该用通用引用作为参数

1
2
3
4
5
template<typename C, typename I>
decltype(auto) getItem2(C&& c, const I &i)
{
return std::forward<C>(c)[i];
}

表达式(x)会被当作引用, 所以下面的代码有坑

1
2
3
4
5
6
7
8
9
10
11
12
decltype(auto) ff1()  // 返回值是 Widget
{
Widget x;
return x;
}

decltype(auto) ff2() // 返回值是 Widget&
{
Widget x;
return (x);
}
经测试, `ff2`返回的是`Widget&`, 但是这个引用的对象已经析构了

4. 怎样看编译器推断的类型?

  • 看IDE
  • 写个错误的模板类等编译出错时看
  • 用boost::type-index
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #include <boost/type_index.hpp>
    using boost::typeindex::type_id_with_cvr;
    template<typename T> void f2(T& param)
    {
    std::cout << "T = "
    << type_id_with_cvr<T>().pretty_name()
    << std::endl;
    std::cout << "param = "
    << type_id_with_cvr<decltype(param)>().pretty_name()
    << std::endl;
    }