C++ 11 改进的类型推断:auto,decltype 以及新的函数声明语法

原文出处:http://www.cprogramming.com/c++11/c++11-auto-decltype-return-value-after-function.html

C++11 引入了几种新的类型推断,可以让你为那些编译器本来就应该知道的事实写更少的代码。当然,我们也需要在必要的时间帮助编译器或者其他开发人员,但是使用 C++ 11,你可以在那些无聊的工作上花费较少的时间,而关注于业务逻辑。

我们从最明显的新特性:auto关键字开始讲起。

auto的乐趣

下面为那些没有阅读过 C++0x 的第一篇文章的读者简要介绍下auto关键字。在 C++11 中,如果编译器可以在声明的位置推断出变量的类型,那么,你就可以不写出变量类型,只使用auto关键字:

int x = 4;
// 现在可以这么写
auto x = 4;

当然,这完全看不出auto的强大之处。当我们结合模板和遍历器的时候,就可以看出其闪光点了:

vector<int> vec;
auto itr = vec.iterator(); // 不需要写 vector<int>::iterator itr

也有适合auto出现的另外的地方。例如,假设你需要类似下面类型的代码:

template <typename BuiltType, typename Builder>
void makeAndProcessObject (const Builder& builder)
{
    BuiltType val = builder.makeObject();
    // 使用 val 干点什么...
}

在这个代码中,我们有两个必须的模板参数:一个是构建器对象自己的类型,第二个是被构建的对象的类型。更糟糕的是,被构建的对象类型不能声明为目标参数。每次调用时,我们必须这么写:

MyObjBuilder builder;
makeAndProcessObject<MyObj>( builder );

但是,auto立马消除了这种难看的代码,因为你再也不需要在构建对象时指明类型。让 C++ 帮你做吧:

template <typename Builder>
void makeAndProcessObject (const Builder& builder)
{
    auto val = builder.makeObject();
    // 使用 val 干点什么...
}

现在,你只需要一个模板参数,这个参数可以在调用函数时轻松推断出来:

MyObjBuilder builder;
makeAndProcessObject( builder );

这对调用者更加友好,从可读性方面说,模板代码也没什么损失——如果说有的话,那也是更容易理解的!

decltype和新返回值语法的乐趣

现在你可能会说——好吧,不错,但如果我要返回这个构建器对象构建出来的对象呢?我还是得提供一个模板参数,因为我需要提供返回值。好吧,事实证明,标准委员会都是聪明人,他们提供了一套漂亮的解决方案解决这个问题。这个解决方案分为两部分:decltype和新的返回值语法。

新的返回值语法

我们先来看看新的、可选的返回值语法,因为它是auto的另外一种应用。在 C 和 C++ 之前版本中,函数的返回值出现在函数之前:

int multiply (int x, int y);

在 C++11 中,如果你愿意,你可以将返回值放在函数声明的最后,将auto放在返回值的位置:

auto multiply (int x, int y) -> int;

那么,为什么要这么做呢?为理解这一点,我们先看一个简单的例子:一个带有枚举的类:

class Person
{
public:
    enum PersonType { ADULT, CHILD, SENIOR };
    void setPersonType (PersonType person_type);
    PersonType getPersonType ();
private:
    PersonType _person_type;
};

我们有一个简单的类,Person,它有一个类型:一个人要么是成人,要么是小孩,要么是老人。这不用多说,但是,当你定义函数的时候会发生什么呢?

第一个函数,设置器,没什么好说的,你可以直接使用枚举类型PersonType,没任何问题:

void Person::setPersonType (PersonType person_type)
{
    _person_type = person_type;
}

但是,第二个函数就有点问题了。最漂亮的代码却无法通过编译:

// 编译器不知道 PersonType 是什么,因为 PersonType 在 Person 类的外面使用
PersonType Person::getPersonType ()
{
    return _person_type;
}

你必须这么写:

Person::PersonType Person::getPersonType ()
{
    return _person_type;
}

这么写才能让返回值正常工作。这不是什么大问题,但是非常容易犯错,并且当使用模板的时候,就更有可能犯错了。

这正是新返回值语法应用的地方。由于返回值出现在函数末端,而不是前部,所以你不需要添加类作用域。当编译器到达返回值的地方时,它已经知道这个函数是Person类的一部分,所以它知道什么是PersonType

auto Person::getPersonType () -> PersonType
{
    return _person_type;
}

好吧,这看起来不错,不过它真的那么有用吗?我们能用这个新语法解决前面提出的问题吗?好吧,暂时还不行。我们再新增加一个概念:decltype

decltype

decltype并不是auto邪恶的一面。auto允许你用特殊的类型声明变量,decltype则允许你从一个变量(或任何表达式)导出类型。这是什么意思?

int x = 3;
decltype(x) y = x; // 等价于 auto y = x;

你可以为很多表达式使用decltype,包括函数的返回值类型。嗯,这听起来很熟悉,不是吗?如果我们这么写:

decltype( builder.makeObject() )

就会给出我们makeObject()函数的返回值类型,允许我们将其指定为makeAndProcessObject的返回值。我们可以将其与新返回值语法结合来写:

template <typename Builder>
auto makeAndProcessObject (const Builder& builder) -> decltype( builder.makeObject() )
{
    auto val = builder.makeObject();
    // 使用 val 干点什么...
    return val;
}

这仅在使用新返回值语法的情形下才可以使用,因为在旧的语法下,我们不能在声明返回类型的地方使用函数参数builder。但是,结合新语法,函数的所有参数同等对待!

auto、引用、指针和const

现在肯定会有一个问题,怎么处理引用:

int& foo();

auto bar = foo(); // int& 还是 int?

简单来说,在 C++ 11 中,对于引用,auto默认使用传值的方式,因此,上面的代码结果是int。但是,你也可以使用&修饰符强制使用引用:

int& foo();

auto bar = foo(); // int
auto& baz = foo(); // int&

另一方面,如果你有一个指针auto,它会自动成为一个指针类型:

int* foo();

auto p_bar = foo(); // int*

不过,你也可以显式指明该变量是一个指针:

int* foo();
auto *p_baz = foo(); // int*

同时,如果需要的话,你也可以在处理引用时,为auto添加const修饰符:

int& foo();

const auto& baz = foo(); // const int&

或者结合指针:

int* foo();
const int* const_foo();
const auto* p_bar = foo(); // const int*
auto p_bar = const_foo(); // const int*

总之,这种感觉是非常自然寻常的,即便在 C++ 的模板中,也遵循同常的规则。

那么,编译器的支持性如何?

就目前而言,GCC 4.4 和 MSVC 10 完全支持上述特性,我已经在产品中使用了这样的代码。这并不是理论上的优点,而是实实在在的。所以,如果你在使用 GCC 时指定-std=c++0x或者使用 MSVC 10 编译代码,现在你就可以使用这些技术了。如果你使用的是其它编译器,可以在这个页面找到 C++ 11 的编译器支持情况。由于标准已经确认,即将在几周内发布(注:本文发表于 2011 年),现在是时候使用了。

Leave a Reply