C++ auto的用法(非常详细)
在现代 C++ 编程中,自动类型推导是非常重要且使用非常广泛的特性之一。在新的 C++ 标准中,可以在任何地方使用 auto 作为类型的占位符,编译器会自动推导出它的实际类型。
在 C++11 中,auto 可以用于声明局部变量和尾部返回值指定类型的函数。在 C++14 中,auto 可以用于没有指定尾部返回类型的函数,同时也可用于 lambda 表达式中参数的声明。未来的标准可能会扩展 auto,使之适用于更多的场景。
1) 当不想指定特定类型时,可以使用下面的形式声明局部变量:
2) 当需要指定类型时,可以使用如下形式声明局部变量:
3) 声明命名 lambda 函数时,格式为:
4) 声明 lambda 表达式的参数和返回值:
5) 当不想指定函数返回值类型时,用声明函数返回类型:
有时,我们必须指定类型。例如,在文章开头的第一个例子中,变量被编译器推导为 char const * 类型。如果目标是使用 std::string,那么就必须显式地指定变量 s 的类型(std::string)。同样,v 的类型被推导为 std::initializer_list<int>。然而,你可能需要的是 std::vector<int>类型,在这种情况下,必须在赋值符的右边显式指定类型。
用 auto 标识符来代替真正的类型有很多好处,下面列出了重要的几条:
1) 不会造成变量未初始化。这是开发者最常见的一个错误(声明没有初始化的特定类型变量)。使用 auto,这种情况不可能发生,因为编译器为了能够正确推导,要求必须对变量初始化化。
2) 使用 auto 可以保证始终使用正确的类型,同时保证不会发生隐式转换。
观察下面这个通过局部变量获取 vector 的大小的例子:
3) 使用 auto 有利于推动良好的面向对象编程实践,例如更倾向于采用接口而不是具体实现。指定的类型越少,代码的通用性越强,并且对未来越开放,这是面向对象编程的一个基本原则。
4) 使用 auto 可以精简代码,同时无须过多关心真正的数据类型。一个非常常见的现象是虽然我们指定了类型,但是我们其实很少关注类型。虽然这种现象在使用迭代器的场景中很常见,但是还有很多场景也是这样。
当想迭代一个可迭代对象时,你根本就不会关心迭代器的类型,你关心的只有迭代器本身。所以,使用 auto 不但可以节省时间(因为迭代器的名字有可能会很长),而且还可以让你把精力集中在业务代码上而不是类型上。
在下面的例子中,第一个 for 循环显式地使用了迭代器的类型,迭代器的类型名很长,长语句代码会降低代码的可读性,而你还必须清楚实际并不关心的迭代器的类型。第二个 for 循环用到了 auto 标识符,看起来很简练,不仅可以节约敲代码的时间,而且你只需要关注迭代器本身:
5) 使用 auto 声明变量提供了一致的编码风格,类型始终位于右侧。如果要动态分配对象,需要在赋值符的左右两边都写出类型,例如 int* p = new int(42)。但是,使用 auto,类型只需在右侧指定一次。
1) auto 标识符只是类型的占位符,不能用于 const/volatile 以及引用类型。如果需要 const/volatile 以及引用类型,则需要显式指定它们。
2) 不能对不可移动的类型使用 auto 标识符:
3) auto 标识符不能用于多字类型,例如 long long、long double 或者 struct foo。但是,对于第一种情况,可能的解决方法是使用字面量或类型别名;对于第二种情况(struct foo),使用 struct/class 这种形式只是为了使 C++ 与 C语言保持兼容,这种情况无论如何都应该避免:
4) 如果使用 auto 标识符但同时也想知道类型,那么只需要把鼠标放在变量上面即可,因为大多数 IDE 都支持这种操作。当然,离开 IDE,这种操作就失效了,只能自己通过初始化表达式推导真正的类型,这意味着可能要搜索代码,因为表达式可能是一个函数调用。
5) auto 标识符可用于指定函数的返回类型。在 C++11 标准中,在函数声明时需要给出尾部返回类型。但是在 C++14 中就没有这个限制了,返回值的类型通过 return 表达式自动推导。如果有多个返回值,它们应该具有相同的类型:
6) 如前所述,auto 不保留 const/volatile 和引用限定符。这会导致 auto 作为函数返回类型的占位符出现问题。为了解释这一点,请考虑前面的 foo.get() 示例。这次,我们有一个名为 proxy_get() 的包装函数,它接受一个对 foo 的引用,调用 get() 并返回 get() 返回的值,即 int&。然而,编译器会将 proxy_get() 的返回类型推导为 int,而不是 int&。尝试将 int 值赋给 int& 类型会失败,从而引发错误:
C++14 对这个问题的解决方法是使用 decltype(auto),这可以确保类型推导的正确性:
7) 从 C++14 开始,lambda 返回类型和参数类型都可以使用 auto。这样的 lambda 被称为泛型 lambda,因为由 lambda 定义的闭包类型具有模板调用运算符的功能。
下面的例子展示了一个泛型 lambda,它接受两个 auto 类型的参数并返回 operator+ 运算后的结果:
在 C++11 中,auto 可以用于声明局部变量和尾部返回值指定类型的函数。在 C++14 中,auto 可以用于没有指定尾部返回类型的函数,同时也可用于 lambda 表达式中参数的声明。未来的标准可能会扩展 auto,使之适用于更多的场景。
C++ auto的使用
以下情形可以考虑使用 auto 作为实际类型的占位符:1) 当不想指定特定类型时,可以使用下面的形式声明局部变量:
auto name=expression;例如:
auto i = 42; // int auto d = 42.5; // double auto s = "text"; // const char * auto v = {1,2,3}; // std::initializer_list<int>
2) 当需要指定类型时,可以使用如下形式声明局部变量:
auto name:type-id {expression}例如:
auto b = new char[10]{ 0 }; // char* auto s1 = std::string {"text"}; // std::string auto v1 = std::vector<int> { 1, 2, 3 }; // std::vector<int> auto p = std::make_shared<int>(42); // std::shared_ptr<int>
3) 声明命名 lambda 函数时,格式为:
auto name=lambda-expression;例如:
auto upper = [](char const c) {return toupper(c); };
4) 声明 lambda 表达式的参数和返回值:
auto add = [](auto const a, auto const b) {return a + b;};
5) 当不想指定函数返回值类型时,用声明函数返回类型:
template <typename F, typename T> auto apply(F&& f, T value) { return f(value); }
C++ auto的工作原理
auto 标识符基本上可以说是实际类型的占位符。使用 auto 时,编译器会从以下情况推导出实际类型:- 当 auto 用于声明变量时,从用于初始化变量的表达式类型推导。
- 当 auto 用作函数返回类型的占位符时,从函数的尾部返回类型或者函数返回的表达式推导。
有时,我们必须指定类型。例如,在文章开头的第一个例子中,变量被编译器推导为 char const * 类型。如果目标是使用 std::string,那么就必须显式地指定变量 s 的类型(std::string)。同样,v 的类型被推导为 std::initializer_list<int>。然而,你可能需要的是 std::vector<int>类型,在这种情况下,必须在赋值符的右边显式指定类型。
用 auto 标识符来代替真正的类型有很多好处,下面列出了重要的几条:
1) 不会造成变量未初始化。这是开发者最常见的一个错误(声明没有初始化的特定类型变量)。使用 auto,这种情况不可能发生,因为编译器为了能够正确推导,要求必须对变量初始化化。
2) 使用 auto 可以保证始终使用正确的类型,同时保证不会发生隐式转换。
观察下面这个通过局部变量获取 vector 的大小的例子:
auto v = std::vector<int>{ 1, 2, 3 }; // implicit conversion, possible loss of data int size1 = v.size(); // OK auto size2 = v.size(); // ill-formed (warning in gcc/clang, error in VC++) auto size3 = int{ v.size() };在第一个例子中,变量的类型是 int类型,即使 size() 方法的返回值类型是 size_t。这就意味着发生了从 size_t 到 int 类型的隐式转换。但是,用 auto 关键字就可以推导出正确的类型,即 size_t 类型。
3) 使用 auto 有利于推动良好的面向对象编程实践,例如更倾向于采用接口而不是具体实现。指定的类型越少,代码的通用性越强,并且对未来越开放,这是面向对象编程的一个基本原则。
4) 使用 auto 可以精简代码,同时无须过多关心真正的数据类型。一个非常常见的现象是虽然我们指定了类型,但是我们其实很少关注类型。虽然这种现象在使用迭代器的场景中很常见,但是还有很多场景也是这样。
当想迭代一个可迭代对象时,你根本就不会关心迭代器的类型,你关心的只有迭代器本身。所以,使用 auto 不但可以节省时间(因为迭代器的名字有可能会很长),而且还可以让你把精力集中在业务代码上而不是类型上。
在下面的例子中,第一个 for 循环显式地使用了迭代器的类型,迭代器的类型名很长,长语句代码会降低代码的可读性,而你还必须清楚实际并不关心的迭代器的类型。第二个 for 循环用到了 auto 标识符,看起来很简练,不仅可以节约敲代码的时间,而且你只需要关注迭代器本身:
std::map<int, std::string> m; for (std::map<int, std::string>::const_iterator it = m.cbegin(); it != m.cend(); ++it) { /*...*/ } for (auto it = m.cbegin(); it != m.cend(); ++it) { /*...*/ }
5) 使用 auto 声明变量提供了一致的编码风格,类型始终位于右侧。如果要动态分配对象,需要在赋值符的左右两边都写出类型,例如 int* p = new int(42)。但是,使用 auto,类型只需在右侧指定一次。
C++ auto的使用注意事项
使用 auto 有一些需要注意的问题:1) auto 标识符只是类型的占位符,不能用于 const/volatile 以及引用类型。如果需要 const/volatile 以及引用类型,则需要显式指定它们。
class foo { int x_; public: foo(int const x = 0) : x_{ x } {} int& get() { return x_; } }; foo f(42); auto x = f.get(); x = 100; std::cout << f.get() << '\n'; // prints 42程序中,foo.get() 返回的是一个 int 类型的引用。当变量 x 根据函数返回值进行初始化时,编译器推导的结果是 int 类型而非 int&。因此,对 x 所做的任何修改都不会改变 foo.x_。为此,我们应该使用 auto&。
2) 不能对不可移动的类型使用 auto 标识符:
auto ai = std::atomic<int>(42); // error
3) auto 标识符不能用于多字类型,例如 long long、long double 或者 struct foo。但是,对于第一种情况,可能的解决方法是使用字面量或类型别名;对于第二种情况(struct foo),使用 struct/class 这种形式只是为了使 C++ 与 C语言保持兼容,这种情况无论如何都应该避免:
auto l1 = long long{ 42 }; // error using llong = long long; auto l2 = llong{ 42 }; // OK auto l3 = 42LL; // OK
4) 如果使用 auto 标识符但同时也想知道类型,那么只需要把鼠标放在变量上面即可,因为大多数 IDE 都支持这种操作。当然,离开 IDE,这种操作就失效了,只能自己通过初始化表达式推导真正的类型,这意味着可能要搜索代码,因为表达式可能是一个函数调用。
5) auto 标识符可用于指定函数的返回类型。在 C++11 标准中,在函数声明时需要给出尾部返回类型。但是在 C++14 中就没有这个限制了,返回值的类型通过 return 表达式自动推导。如果有多个返回值,它们应该具有相同的类型:
// C++11 auto func1(int const i) -> int { return 2*i; } // C++14 auto func2(int const i) { return 2*i; }
6) 如前所述,auto 不保留 const/volatile 和引用限定符。这会导致 auto 作为函数返回类型的占位符出现问题。为了解释这一点,请考虑前面的 foo.get() 示例。这次,我们有一个名为 proxy_get() 的包装函数,它接受一个对 foo 的引用,调用 get() 并返回 get() 返回的值,即 int&。然而,编译器会将 proxy_get() 的返回类型推导为 int,而不是 int&。尝试将 int 值赋给 int& 类型会失败,从而引发错误:
class foo { int x_; public: foo(int const x = 0) : x_{ x } {} int& get() { return x_; } }; auto proxy_get(foo& f) { return f.get(); } auto f = foo{ 42 }; auto& x = proxy_get(f); // cannot convert from 'int' to 'int &'为了解决这个问题,我们需要指定函数返回 auto&。然而,这对于模板来说是一个问题,因为模板在完整转发返回值时无法判断返回类型是值还是引用。
C++14 对这个问题的解决方法是使用 decltype(auto),这可以确保类型推导的正确性:
decltype(auto) proxy_get(foo& f) { return f.get(); } auto f = foo{ 42 }; decltype(auto) x = proxy_get(f);decltype 标识符用于检查实体或者表达式的声明类型。当声明类型很麻烦或根本不可能用标准表示法声明时,它很有用。这样的例子包括 lambda 类型和依赖模板参数的类型。
7) 从 C++14 开始,lambda 返回类型和参数类型都可以使用 auto。这样的 lambda 被称为泛型 lambda,因为由 lambda 定义的闭包类型具有模板调用运算符的功能。
下面的例子展示了一个泛型 lambda,它接受两个 auto 类型的参数并返回 operator+ 运算后的结果:
auto ladd = [] (auto const a, auto const b) { return a + b; }; struct { template<typename T, typename U> auto operator () (T const a, U const b) const { return a+b; } } L;这样的 lambda 可用于将定义了 operator+ 的任何对象相加,如以下代码片段所示:
auto i = ladd(40, 2); // 42 auto s = ladd("forty"s, "two"s); // "fortytwo"s在这个例子中,我们将 ladd lambda 用于两个整数相加以及两个字符串(std::string)对象拼接(字符串运用了 C++14 中定义的字面量运算符""s)。