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

来源:互联网 发布:致幻蘑菇 知乎 编辑:程序博客网 时间:2024/05/17 06:48

推导类型

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]ii*/

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;}
constructcopyconstructmoveProgram ended with exit code: 0

结果就非常明显了。
具体关于move和forward的使用细节见:
c++11 中的 move 与 forward

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

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

这会导致不确定性行为。

1 0
原创粉丝点击