[C++]高效使用c++11--理解auto类型推导

推导类型

1. 理解类型推导

auto的推导方式和template是一样的,所以我们首先来介绍template是如何推导类型的。

template <typename T>
void f(const T& orig) {
    cout << __PRETTY_FUNCTION__ << endl;
    cout << typeid (orig).name() << endl;
    cout << typeid (T).name() << endl;
}
    int x = 10;
    f(x);
/*
void f(const T &) [T = int]
i
i
*/

T和orig的类型一样的,这很奇怪吧。实际上,template类型推导有三个情况:

    1. orig是一个指针或者引用类型,但不是全局引用(universal reference)
    1. orig是一个全局引用。
    1. orig即使不是指针也不是引用。
template <typename T>
void f(ParamType param);
f(expr)

情况1 :ParamType是一个指针或者引用类型,但不是全局引用(universal reference)

在这种情况下,

    1. 如果expr的类型是一个引用,忽略引用的部分。
    1. 把expr的类型与ParamType的类型比较,用来判断T的类型。

例如:

template <typename T>
void f(T& param) {
    cout << __PRETTY_FUNCTION__ << endl;
}
int main(int argc, char *argv[]) {
    int y = 10;
    f(y);
    const int x = y;
    f(x);
    const int& z = y;
    f(z); // ignore the reference.
    return 0;
}
void f(T &) [T = int]
void f(T &) [T = const int]
void f(T &) [T = const int]

这就是为什么一个const对象传给模板后是安全的,因为const性质会成为模板推导的一部分。

我们注意到第三个例子中,T被推导为const int,是因为忽略了&, 如果不这样,ParamType 会被推导为const int&&,这是不被允许的。

我们这里提及的都是左值引用,实际上右值引用也是一样的,但是右值引用只能传递给右值引用,虽然这个类型推导关系不大。

我们来做一个小小的修改。

template <typename T>
void f(const T& param) {
    cout << __PRETTY_FUNCTION__ << endl;
}
int main(int argc, char *argv[]) {
    int y = 10;
    f(y);
    const int x = y;
    f(x);
    const int& z = y;
    f(z); // ignore the reference.
    return 0;
}
void f(const T &) [T = int]
void f(const T &) [T = int]
void f(const T &) [T = int]

同样的,T的引用被忽略了,const属性也被忽略了。因为对于param而言总是const。

对于指针:

template <typename T>
void f(T* param) {
    cout << __PRETTY_FUNCTION__ << endl;
}
int main(int argc, char *argv[]) {
    int y = 10;
    f(&y);
    const int x = y;
    f(&x);
    const int& z = y;
    f(&z); // ignore the reference.
    const int* p = &z;
    f(p);
    return 0;
}
void f(T *) [T = int]
void f(T *) [T = const int]
void f(T *) [T = const int]
void f(T *) [T = const int]

分析也一样。

情况2:ParamType是一个全局引用。

全局引用是T&&类型,也就是右值引用。这种情况稍微有一些不同。

    1. 如果expr是一个左值引用,那么T和ParamType都会被推导为左值引用。
    1. 如果expr是一个右值,那么就使用通常的推导方法。

例如:

template <typename T>
void f(T&& param) {
    cout << __PRETTY_FUNCTION__ << endl;
}
int main(int argc, char *argv[]) {
    int y = 10;
    f(y);
    const int x = y;
    f(x);
    const int& z = y;
    f(z);
    f(10);
    return 0;
}
void f(T &&) [T = int &] //  param为int &
void f(T &&) [T = const int &]
void f(T &&) [T = const int &]
void f(T &&) [T = int] //  param 为int&&

关键是,当使用全局引用时,类型推导会区别于右值引用和左值引用。

情况3: ParamType既不是指针也不是引用时

    1. 如果expr是一个引用,忽略引用。
    1. 如果expr是一个const, 忽略const。如果expr是一个volatile,忽略它。

例如:

template <typename T>
void f(T param) {
    cout << __PRETTY_FUNCTION__ << endl;
}
int main(int argc, char *argv[]) {
    int y = 10;
    f(y);
    const int x = y;
    f(x);
    const int& z = y;
    f(z);
    return 0;
}
void f(T) [T = int]
void f(T) [T = int]
void f(T) [T = int]

因为这里是按值传递,所以本身对象的性质并不会传递给他的拷贝对象。

前面提到在类型推导时,const引用或者constpointer的const属性会被保留,但如果expr是一个const指针指向const对象,而expr被传递给按值传递的函数,那么情况会怎么样?

template <typename T>
void f(T param) {
    cout << __PRETTY_FUNCTION__ << endl;
}
int main(int argc, char *argv[]) {
    const char* const ptr = "yanzexin";
    f(ptr);
    return 0;
}
void f(T) [T = const char *]
    const char* const ptr = "yanzexin";

第一个const,指不可以修改这个指针指向的对象。

第二个const,指不可以修改指针指向的对象的值。

在传递过程中,指针的const属性被保留,但指针指向对象的const属性被忽略了。

对于数组

数组通常情况下都会被理解为指向数组第一个元素的指针。

template <typename T>
void f(T param) {
    cout << __PRETTY_FUNCTION__ << endl;
}
int main(int argc, char *argv[]) {
    const char name[] = "stary";
    f(name);
    const int phone[] = {1, 2};
    f(phone);
    return 0;
}
void f(T) [T = const char *]
void f(T) [T = const int *]

但如果我们真的希望传递的是一个数组,我们可以使用引用。

template <typename T>
void f(T& param) {
    cout << __PRETTY_FUNCTION__ << endl;
}
int main(int argc, char *argv[]) {
    const char name[] = "stary";
    f(name);
    const int phone[] = {1, 2};
    f(phone);
    return 0;
}
void f(T &) [T = char const[6]]
void f(T &) [T = int const[2]]

T会真正的被推导为数组。由此param的类型实际上就是const char&[6]。 所以我们甚至可以直接推导出数组的大小。

template <typename T, size_t N>
void f(T(&) [N]) {
    cout << __PRETTY_FUNCTION__ << endl;
}
int main(int argc, char *argv[]) {
    const char name[] = "stary";
    f(name);
    const int phone[] = {1, 2};
    f(phone);
    return 0;
}
void f(T (&)[N]) [T = const char, N = 6]
void f(T (&)[N]) [T = const int, N = 2]

对于函数

函数实际上也是和数组一样,会被自动推导到指针。如果希望推导成引用,方法是一样的。

template <typename T>
void f(T param) {
    cout << __PRETTY_FUNCTION__ << endl;
}
void func(double, int) {
}
int main(int argc, char *argv[]) {
    f(func);
    return 0;
}
void f(T) [T = void (*)(double, int)]
template <typename T>
void f(T& param) {
    cout << __PRETTY_FUNCTION__ << endl;
}
void func(double, int) {
}
int main(int argc, char *argv[]) {
    f(func);
    return 0;
}
void f(T &) [T = void (double, int)]

关键点:

    1. 当推导类型为指针或非全局引用,引用性会被忽略。
    1. 当推导类型为全局引用时,左值引用被推导为左值引用,右值引用被推导为右值引用。
    1. 当为按值传递时,推导的引用和const属性被忽略。
    1. 数组和函数会被推导为指针。

2. 理解auto的类型推导

auto就是使用template的推导方式,实际上存在一种直接的映射,把template的推导和auto的类型推导联系起来。

int main(int argc, char *argv[]) {
  auto x = 17; // x = int
  auto& y = x; // y = int&
  const auto& i = x; // i = const int&
  auto z = y; // z = int
  const auto f = y; // f = const int
  auto& m = i; // m = const int&
  auto&& t = i; // t = int&
  auto&& t1 = 10; // t1 = int&&
  return 0;
}
int main(int argc, char *argv[]) {
  int A[3] = {1, 2, 3};
  auto a = A; // a = int *
  auto& a1 = A; // a1 = int (&)[3]
  return 0;
}

可以发现和auto确实没什么区别。

实际上,auto和template只有一个很大的区别。

在C++11中给出了一个全新的初始化方法,叫参数列表。

int main(int argc, char *argv[]) {
  int a0 = (1); // a0 = int
  auto a1 = (1); // a1 = int
  int a2 = {1}; // a0 = int
  auto a3 = {1}; // a3 = std::initializer_list<int>
  return 0;
}

但需要注意的是,参数列表在auto中会被推导为 std::initializer_list。

auto中可以把带{}的推导出来,但template是推导不出来的,编译无法通过。

template <typename T>
void test(T orig) {
  cout << __PRETTY_FUNCTION__ << endl;
}

int main(int argc, char *argv[]) {
  test((1));
  test({1}); // error!
  return 0;
}

在C++11中,这就已经没什么问题了。但C++14中还有一小部分的问题需要讨论。

C++14允许auto去指示函数的返回类型,并且允许在lambda表达式中使用auto参数(C++11中不允许),但这个auto的推导是使用template推导方式的,不是使用auto本身的推导方式。这也就是说,返回一个{},是不能通过编译的。

int main(int argc, char *argv[]) {
  list<int> ls;
  auto l = [&ls](auto&& list) {
    ls.insert(ls.end(), list);
  };
  l(1);
  return 0;
}
int main(int argc, char *argv[]) {
  list<int> ls;
  auto l = [&ls](auto&& list) {
    ls = list;
  };
  //  l({1, 2, 3, 4}); error!
  l(list<int> {1, 2, 3, 4});
  return 0;
}

关键点:

auto推导通常情况下和template的推导是一样的,除非一个变量被声明并且是使用的初始化列表。

3. 理解decltype

template <typename container, typename index>
auto re(container& con, index i) {
  return con[i];
}

int main(int argc, char *argv[]) {
  vector<int> a {1, 2, 3, 4, 5, 6};
  re(a, 3) = 10; // 无法通过编译
  cout << a[3] << endl;
  return 0;
}

返回类型是auto,就有前面提到的一样vector []operator返回的是引用类型,但auto会自动把引用类型忽略,从而无法进行修改。但如果我们希望这个函数返回的是真正的引用类型,该怎么做呢?

使用decltype,显式表明返回类型。以下这个实现方法能实现但不够好。原因我们暂时不去解释。

template <typename container, typename index>
auto re(container& con, index i) ->decltype(con[i]) {
  return con[i];
}

int main(int argc, char *argv[]) {
  vector<int> a {1, 2, 3, 4, 5, 6};
  re(a, 3) = 10;
  cout << a[3] << endl;
  return 0;
}

一种更简单的做法是:

template <typename container, typename index>
decltype(auto) re(container&& con, index i) {
  return con[i];
}
int main(int argc, char *argv[]) {
  vector<int> a{1, 2, 3, 4, 5};
  re(a, 3) = 10;
  cout << re(a, 3) << endl;
  return 0;
}

auto表明这个类型需要推导,decltype表示推导使用decltype的方法,也就是根据他实际的类型来返回。这种方法更加好。但需要c++14。

decltype(auto) 不仅可以用作函数的返回类型,也可以用作变量的声明。

int main(int argc, char *argv[]) {
  const int a = 10;
  decltype(auto) b = a;
  return 0;
}

b也是const int!

template <typename container, typename index>
decltype(auto) re(container& con, index i) {
  return con[i];
}

这个容器只能传递非const左值引用。右值引用不能捆绑左值引用。(除非是const的左值引用)

我们之前提到的

template <typename container, typename index>
decltype(auto) re(container&& con, index i) {
  return con[i];
}

做法不是太好。

具体的做法应该是

template <typename container, typename index>
decltype(auto) re(container& con, index i) {
  return forward<container>(con)[i];
}

实际上是这样的C++给出标准,有对于右值引用既可以是左值也可以是右值,有名字的就是左值,没名字的就是右值。

forward只能在模板函数中私用, 它原本是什么类型就返回什么类型。于是我们可以做出这样的实例,来解释。

#include <iostream>
#include <vector>
using namespace std;
class test {
public:
  test() {
    cout << "construct" << endl;
  }
  test(test&& orig) {
    cout << "move" << endl;
  }
  test(test& orig) {
    cout << "copy" << endl;
  }
};
template <typename T>
decltype(auto) f(T&& orig) {
  return forward<T>(orig);
}
int main(int argc, char *argv[]) {
  test b;
  test c = f(b);
  cout << endl;
  test m = f(test());
  return 0;
}
construct
copy

construct
move
Program ended with exit code: 0

结果就非常明显了。

具体关于move和forward的使用细节见:

c++11 中的 move 与 forward

最后一个问题是,decltype((x))为被推导为int&!这里需要注意的是,不要返回临时对象的引用。否则可能会出问题。

decltype(auto) f() {
  int x = 0;
  return (x);
}

这会导致不确定性行为。

时间: 2024-10-26 22:05:38

[C++]高效使用c++11--理解auto类型推导的相关文章

现代C++之理解auto类型推断

理解auto类型推断 上一篇帖子中讲述了模板类型推断,我们知道auto的实现原理是基于模板类型推断的,回顾一下模板类型推断: template <typename T> void f(ParamType param); 使用下面的函数调用: f(expr); 我们看到模板类型推断过程涉及到了模板template.函数f以及参数(包括模板参数和函数参数),调用f的时候,编译器会推断T和ParamType的类型.auto的实现和这三个部分是有着对应关系的.当使用auto声明一个变量,auto关键字

《Effective Modern C++》翻译--条款1: 理解模板类型推导

北京2016年1月9日13:47:17 开始第一章的翻译. 第一章名为 类型推断 分为四个条款: 1理解模板类型推导 2理解auto自动类型推导 3理解decltype操作符 4如何对待推导的类型 第一章 类型推导 C++98有一套单一的类型推导的规则用来推导函数模板.C++11轻微的修改了这些规则并且增加了两个推导规则,一个用于auto,一个用于decltype.接着C++14扩展了auto和decltype可以使用的语境.类型推导的普遍应用将程序员从必须拼写那些显然多余的类型中解放了出来,它

初窥C++11:自动类型推导与类型获取

auto 话说C语言还处于K&R时代,也有auto a = 1;的写法.中文译过来叫自动变量,跟c++11的不同,C语言的auto a = 1;相当与 auto int a = 1;语句. 而C++11的auto是有着严格的类型推导出来的.以前是这么写 int a = 1; 现在,编译器知道a是int型了.所以可以这么写 auto a = 1; 对于类型比较长的,如vector<string>::iterator这类的,能少敲些字符了. 如果仅仅就这点作用,那么对编程实在没什么太大的益

auto类型推导

引言 auto : 类型推导. 在使用c++的时候会经常使用, 就像在考虑STL时迭代器类型, 写模板的时候使用auto能少写代码, 也能帮助我们避免一些隐患的细节. auto初始化 使用auto型别推导要求必须在定义时初始化, 毕竟需要根据对象的类型推导左值对象的型别. auto j; // error. 必须初始化 auto i = 0; // i 推导型别为 int vector<int> v; auto vv = v.cbegin(); // vv 推导型别为 const int* 但

《Effective Modern C++》翻译--条款2: 理解auto自动类型推导

条款2: 理解auto自动类型推导 如果你已经读过条款1关于模板类型推导的内容,那么你几乎已经知道了关于auto类型推导的全部.至于为什么auto类型推导就是模板类型推导只有一个地方感到好奇.那是什么呢?即模板类型推导包括了模板.函数和参数,而auto类型推断不用与这些打交道. 这当然是真的,但是没关系.模板类型推导和auto自动类型推导是直接匹配的.从字面上看,就是从一个算法转换到另一个算法而已. 在条款1中,阐述模板类型推导采用的是常规的函数模板: template<typename T>

《Effective Modern C++》翻译--条款2: 理解auto自己主动类型推导

条款2: 理解auto自己主动类型推导 假设你已经读过条款1关于模板类型推导的内容,那么你差点儿已经知道了关于auto类型推导的所有. 至于为什么auto类型推导就是模板类型推导仅仅有一个地方感到好奇.那是什么呢?即模板类型推导包含了模板.函数和參数,而auto类型判断不用与这些打交道. 这当然是真的.可是没关系. 模板类型推导和auto自己主动类型推导是直接匹配的. 从字面上看,就是从一个算法转换到还有一个算法而已. 在条款1中.阐述模板类型推导採用的是常规的函数模板: template<ty

c++11——auto,decltype类型推导

c++11中引入了auto和decltype关键字实现类型推导,通过这两个关键字不仅能够方便的获取复杂的类型,而且还能简化书写,提高编码效率.     auto和decltype的类型推导都是编译器在编译的时候完成的,auto是通过定义auto变量时候给出的表达式的值推导出实际类型,并且在声明auto变量时必须马上初始化:decltype通过表达式的值推导出实际的类型,但是可以只声明变量,而不赋值. auto类型推导 1. auto推导 auto x = 5; //被编译器推导为int类型 au

《Effective Modern C++》读书笔记 Item 2 auto的类型推导

注意: 还要学习一个 ↑↑↑↑ 这样的方框里的片段完全不来自于原书,而是我自己的理解. Item 2 Understand auto type deduction - auto类型推导 在C++11之前,auto 关键字一直是用于声明自动储存类型的变量时使用的,基本上没有什么实际作用,地位和 export 关键字(用于向编译单元之外导出模板,也在C++11中被取消)类似. 在C++11中,auto 终于不再废材,终于具备了类似C#中 var 关键字的效果,可以自动推导出变量的类型,可以少打几个字

item 1:理解template类型的推导

item 1: 理解template类型的推导 一些用户对复杂的系统会忽略它怎么工作,怎么设计的,但是很高兴去知道它完成的一些事.通过这样的方式,c++中的template类型的推导取得了巨大的成功.数以万计的程序员曾传过参数给template函数,并得到了满意的结果.尽管很多那些程序员很难给出比朦胧的描述更多的东西,比如那些被推导的函数是怎么使用类型来推导的. 如果你也是其中的一员,我这有好消息和坏消息给你.好消息是template类型的推导是现代c++最令人惊叹特性之一(auto)的基础.如