首页 > 编程笔记 > C++笔记 阅读:42

C++ auto的用法(非常详细)

在现代 C++ 编程中,自动类型推导是非常重要且使用非常广泛的特性之一。在新的 C++ 标准中,可以在任何地方使用 auto 作为类型的占位符,编译器会自动推导出它的实际类型。

在 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 时,编译器会从以下情况推导出实际类型:
有时,我们必须指定类型。例如,在文章开头的第一个例子中,变量被编译器推导为 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)。

相关文章