[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);
}

这会导致不确定性行为。

阅读更多
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/stary_yan/article/details/52014191
个人分类: C++编程
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭