美文网首页Effective Modern C++
【Effective Modern C++(1)】类型推断

【Effective Modern C++(1)】类型推断

作者: downdemo | 来源:发表于2018-11-23 10:16 被阅读35次

01 理解模板类型推断

  • 模板类型推断是auto的基础,但部分特殊情况下模板推断的机制不适用于auto
  • 模板的形式可以看成如下伪代码
template<typename T>
void f(paramType param);
  • 调用可以看成
f(expr);
  • 编译期间,编译器用expr推断两个类型:T的类型和ParamType的类型。这些类型通常不同,因为ParamType通常包含限定符,比如
template<typename T>
void f(const T& param); // ParamType是const T&

int x = 0;
f(x); // T被推断为int,ParamType被推断为const int&
  • T的类型推断不仅依赖于expr,也依赖于ParamType, 有三种情况
    • ParamType是指针或引用,但不是转发引用
    • ParamType是转发引用
    • ParamType既不是指针也不是引用

情形1:ParamType是指针/引用类型

  • 如果expr的类型是引用,忽略引用再用匹配expr的ParamType类型来确定T
template<typename T>
void f(T& param);

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

f(x); // T是int,ParamType是int&
f(cx); // T是const int,ParamType是const int&
f(rx); // T是const int,ParamType是const int&
  • 如果把f的参数类型从T&改为const T&,如果expr是引用类型,引用部分仍会被忽略,但有一些微小的不同,T不用再推断为const
template<typename T>
void f(const T& param);

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

f(x); // T是int,ParamType是const int&
f(cx); // T是int,ParamType是const int&
f(rx); // T是int,ParamType是const int&
  • 如果param是指针(或pointer to const),情况一致
template<typename T>
void f(const T* param);

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

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

情形2:ParamType是转发引用

  • 如果expr是左值,T和ParamType都推断为左值引用。这有两点非常特殊
    • 这是T被推断为引用的唯一情形
    • ParamType使用右值引用语法,却被推断为左值引用
  • 如果expr是右值,适用情形1的正常规则
template<typename T>
void f(T&& param);

int x = 27;
const int cx = x;
const int& rx = x;
int&& rr = 27; // rr是右值引用,但也是左值

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

情形3:ParamType不是引用或指针

  • 如果expr是引用,忽略引用部分,如果还有cv限定符也忽略,这也是唯一忽略cv限定符的情形
template<typename T>
void f(T param);

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

f(x); // T和ParamType都是int
f(cx); // T和ParamType都是int
f(rx); // T和ParamType都是int
  • 如果expr是指针,则保留cv限定符
template<typename T>
void f(T param);

const char* const ptr = "Fun with pointers";
const char name[] = "downdemo";
f(ptr); // T和ParamType都是const char* const
f(name); // T和ParamType都是const char*

特殊情形1:expr是数组名

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

const char name[9] = "downdemo";
f1(name); // T和ParamType都是const char*
f2(name); // T是const char[9],ParamType是const char(&)[9],即const char(&param)[9]
f3(name); // 同上,T是const char[9],ParamType是const char(&)[9]
  • 声明ParamType为数组的引用(T(&)[N])能用来创建推断数组元素数量的模板
template <typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept
{
    return N;
}
const char name[] = "downdemo";
int a[arraySize(name)]; // int a[9]

特殊情形2:expr是函数名

void someFunc(int, double);
template<typename T> void f1(T param);
template<typename T> void f2(T& param);
template<typename T> void f3(T&& param);

f1(someFunc); // 推断为ptr-to-fun,T和ParamType都是void(*)(int, double)
f2(someFunc); // 推断为ref-to-fun,T和ParamType都是void(&)(int, double)
f3(someFunc); // 同上,T和ParamType都是void(&)(int, double)

02 理解auto类型推断

  • auto类型推断几乎和模板类型推断一致
  • 调用模板时,编译器根据expr推断T和ParamType的类型。当变量用auto声明时,auto就扮演了模板中的T的角色,变量的类型修饰符则扮演ParamType的角色
  • 为了推断变量类型,编译器表现得好比每个声明对应一个模板,模板的调用就相当于对应的初始化表达式
auto x = 27;
const auto cx = x;
const auto& rx = x;

template<typename T> // 用来推断x类型的概念上假想的模板
void func_for_x(T param);
func_for_x(27); // 假想的调用: param的推断类型就是x的类型

template<typename T> // 用来推断cx类型的概念上假想的模板
void func_for_cx(const T param);
func_for_cx(x); // 假想的调用: param的推断类型就是cx的类型

template<typename T> // 用来推断rx类型的概念上假想的模板
void func_for_rx(const T& param);
func_for_rx(x); // 假想的调用: param的推断类型就是rx的类型
  • auto的推断适用模板推断机制的三种情形:T&、T&&和T
auto x = 27; // int x
const auto cx = x; // const int cx
const auto& rx = x; // const int& rx
auto&& uref1 = x; // int& uref1
auto&& uref2 = cx; // const int& uref2
auto&& uref3 = 27; // int&& uref3
  • auto对数组和指针的推断也和模板一致
const char name[] = "downdemo"; // 数组类型是const char[9]
auto arr1 = name; // const char* arr1
auto& arr2 = name; // const char (&arr2)[9]

void someFunc(int, double); // 函数类型是void(int, double)
auto func1 = someFunc; // void (*func1)(int, double)
auto& func2 = someFunc; // void (&func2)(int, double)
  • auto推断唯一不同于模板推断的情形是C++11的初始化列表,下面实现的都是同样的赋值功能
// C++98
int x1 = 27;
int x2(27);
// C++11
int x3 = { 27 };
int x4{ 27 };
  • 但对于auto,这些赋值有不同的意义
auto x1 = 27; // int x1
auto x2(27); // int x2
auto x3 = { 27 }; // std::initializer_list<int> x3
auto x4{ 27 }; // C++11为std::initializer_list<int> x4,C++14为int x4
  • 如果初始化列表中元素类型不同,则无法推断
auto x5 = { 1, 2, 3.0 }; // 错误:不能为std::initializer_list<T>推断T
  • C++14中禁止对auto用std::initializer_list直接初始化,而必须用=,除非列表中只有一个元素,这时不会将其视为initializer_list
auto x1 = { 1, 2 }; // C++14中必须用=
auto x2 { 1 }; // 保留了单元素列表的直接初始化,但不会将其视为initializer_list
  • 而模板不支持ParamType为T,expr为初始化列表的推断,不会将其假设为std::initializer_list,这就是auto推断和模板推断唯一的不同之处
auto x = { 11, 23, 9 }; // x类型是std::initializer_list<int>

template<typename T> // 等价于x声明的模板
void f(T param);

f({ 11, 23, 9 }); // 错误:不能推断T的类型
  • 不过对模板指定ParamType为std::initializer_list<T>则可以推断T
template<typename T>
void f(std::initializer_list<T> initList);

f({ 11, 23, 9 }); // T被推断为int,initList类型是std::initializer_list<int>
  • 对于C++11,auto的介绍就到此为止了

C++14的auto

  • C++14中,auto可以作为函数返回类型,并且lambda可以将参数声明为auto
auto f() { return 1; }
auto g = [](auto x) {return x; };
  • 但此时auto仍然使用的是模板实参演绎的机制,因此返回类型为auto的函数如果返回一个初始化列表,则会出错
auto newInitList() { return { 1 }; } // 错误
  • 将参数声明为auto的lambda同理
std::vector<int> v { 2, 4, 6};
auto resetV = [&v](const auto& newValue) { v = newValue; }; // C++14
resetV({ 1, 2, 3 }); // 错误

03 理解decltype

  • 给定一个名称或表达式,decltype会不出所料地推断出预期的确切类型
const int i = 0; // decltype(i)是const int

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

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

Widget w; // decltype(w)是Widget
if (f(w)) … // decltype(f(w))是bool

int a[] {1, 2, 3}; // decltype(a)是int[3]

template<typename T> // std::vector的简化版
class vector {
public:
    …
    T& operator[](std::size_t index);
    …
};
vector<int> v; // decltype(v)是vector<int>
…
if (v[0] == 0) … // decltype(v[0])是int&
  • 在C++11中,decltype的主要用法可能是为返回类型依赖于参数类型的函数声明模板,比如写一个使用索引访问容器的函数,返回类型是元素类型的引用,依赖于元素类型T,而使用decltype则能轻松地表达这点
template<typename Container, typename Index>
auto authAndAccess(Container& c, Index i) -> decltype(c[i])
{
    authenticateUser(); // 验证用户有效
    return c[i];
}
  • 上述的auto没有做任何事,只是表示使用类型推断,推断使用的是decltype
  • C++11允许推断single-statement lambda的返回类型,C++14把这点扩展到了所有lambda和函数。对于上例,C++14允许省略尾置返回类型,只留下auto,这样编译器将从函数的实现推断返回类型
template<typename Container, typename Index>
auto authAndAccess(Container& c, Index i)
{
    authenticateUser(); // 验证用户有效
    return c[i];
}
  • 之前提到过,auto作为函数返回类型使用的是模板推断机制,而在这个例子中,这将造成问题。operator[]返回元素引用,类型为T&,但模板的推断会忽略引用,因此下面的用法是错误的
std::deque<int> d;
…
authAndAccess(d, 5) = 10; // 返回d[5]然后赋值为10,但不能通过编译
  • d[5]返回int&,auto推断为int,因此返回的实际是一个右值的整型字面值,上述行为相当于把10赋给一个右值,这显然是错误的
  • 为了得到期望的返回类型,需要对返回类型使用decltype的推断机制,C++14中允许将返回类型声明为decltype(auto)来实现这点
template<typename Container, typename Index>
decltype(auto)
authAndAccess(Container& c, Index i)
{
    authenticateUser();
    return c[i];
}
  • decltype(auto)不仅限于作为函数返回类型,也可以作为变量声明类型
Widget w;
const Widget& cw = w;
auto myWidget1 = cw; // auto类型推断:myWidget1类型是Widget
decltype(auto) myWidget2 = cw; // decltype类型推断:myWidget2类型是const Widget&
  • 但还有一些问题,容器传的是non-const左值引用,这就无法接受右值
template<typename Container, typename Index>
decltype(auto) authAndAccess(Container& c, Index i);
  • 比如一个右值容器是一个调用authAndAccess后就销毁的临时对象
std::deque<std::string> makeStringDeque(); // 工厂函数
// 拷贝从makeStringDeque返回的第五个元素
auto s = authAndAccess(makeStringDeque(), 5);
  • 为了支持这种用法,需要允许接受左值和右值。重载是可行的(一个将参数声明为左值引用,另一个声明为右值引用),但要维护两个函数。避免这点的方法是使用能绑定左值和右值的转发引用作为参数,并使用std::forward转发原有类型
// 最终的C++14版本,c现在是一个转发引用
template<typename Container, typename Index>
decltype(auto) authAndAccess(Container&& c, Index i)
{
    authenticateUser();
    return std::forward<Container>(c)[i];
}
  • 如果要使用C++11版本,只需要多指定返回类型
// 最终的C++11版本,将decltype(auto)改为auto结合尾置返回类型
template<typename Container, typename Index>
auto
authAndAccess(Container&& c, Index i) // version
-> decltype(std::forward<Container>(c)[i])
{
    authenticateUser();
    return std::forward<Container>(c)[i];
}

decltype的特殊情况

  • 如果表达式是解引用,decltype会推断为引用类型
int* p; // decltype(*p)是int&
  • 赋值是会产生引用的一类典型表达式,引用的类型就是左值的类型
int i = 0; // decltype(i=1)是int&
int n = 4;
decltype(i=1) r = n;
r = 5;
std::cout << i << n << r; // 055
  • 如果表达式加上一层或多层括号,编译器会将其看作表达式,变量是一种可以作为赋值语句左值的特殊表达式,因此也得到引用类型。decltype((variable))结果永远是引用,declytpe(variable)只有当变量本身是引用时才是引用
int i; // decltype((i))是int&
  • 在C++14的decltype(auto)中,这将导致返回局部变量的引用的隐患
decltype(auto) f1()
{
    int x = 0;
    …
    return x; // decltype(x)是int,因此返回int
}
decltype(auto) f2()
{
    int x = 0;
    …
    return (x); // decltype((x))是int&,因此返回了局部变量的引用
}

04 查看推断类型的方法

  • 使用IDE时将鼠标停留在变量上
鼠标放在x上
  • 利用编译诊断信息,比如写一个类模板声明但不定义,用这个模板创建实例时将出错,编译将提示错误原因
template<typename T> class TD;

TD<decltype(x)> xType; // 未定义类模板,错误信息将提示x类型
// 比如对int x报错如下
error C2079: “xType”使用未定义的 class“TD<int>”
  • 使用type_id和std::type_info::name,但这不太可靠,因为引用会被忽略
template<typename T>
void f(T& param)
{
    using std::cout;
    cout << "T = " << typeid(T).name() << '\n';
    cout << "param = " << typeid(param).name() << '\n';
}
  • 使用Boost.TypeIndex可以得到精确类型
#include <boost/type_index.hpp>

template<typename T>
void f(const T& param)
{
    using std::cout;
    using boost::typeindex::type_id_with_cvr;
    cout << "T = " << type_id_with_cvr<T>().pretty_name() << '\n';
    cout << "param = " << type_id_with_cvr<decltype(param)>().pretty_name() << '\n';
}

相关文章

网友评论

    本文标题:【Effective Modern C++(1)】类型推断

    本文链接:https://www.haomeiwen.com/subject/uzwdqqtx.html