GCC 4.5 中的 C++0x 特性支持

如果您是 GCC C++ 编译器的众多用户之一,就应该了解即将发布的 C++0x 规范提供的新特性,以及在 GCC 4.3 中支持的特性。

Arpan Sen, 独立作家

Arpan Sen 是致力于电子设计自动化行业的软件开发首席工程师。他使用各种 UNIX 版本(包括 Solaris、SunOS、HP-UX 和 IRIX)以及 Linux 和 Microsoft Windows 已经多年。他热衷于各种软件性能优化技术、图论和并行计算。Arpan 获得了软件系统硕士学位。



2011 年 5 月 23 日

简介

编译 C++0x 代码

只能通过使用 g++ 编译器的 –std=c++0x–std=gnu++0x 命令行选项编译 C++0x 代码。

GNU Compiler Collection (GCC) 是大多数人使用的 C++ 编译器,它已经率先支持即将发布的 C++0x 标准中的特性(更多信息的链接见 参考资料)。本文主要讨论 GCC 4.5 支持的几个 C++0x 特性,包括静态断言、初始化器(initializer)列表、类型窄化(type narrowing)、auto 关键字的新语义、lambda 函数和可变参数模板。其中一些特性(比如静态断言)是在 GCC 4.3 中首次出现的,而 lambda 函数是在 GCC 4.5 中首次出现的。如果希望及早熟悉 C++0x,可以考虑找一份标准草案,下载 GCC 4.5(见 参考资料)。


静态断言

有时候,可移植的代码在客户站点上崩溃了,因为整数的大小并不是您假设的 4 字节。C++0x 中的 static_assert 构造可以准确地跟踪这类问题,可以在编译时使用这种构造,当需要把源代码迁移到其他平台时非常有用。Boost 库(见 参考资料)支持静态断言已经有一段时间了,但是静态断言成为语言核心的一部分(static_assertC++0x 关键字)意味着不再需要头文件(见 清单 1)。

清单 1. 学习使用静态断言
// no headers
// using static assertion in global scope
static_assert(sizeof(int) == 4, "Integer sizes expected to be 4");

int main()
{
    return 0;
}

在我的 64 位企业 Linux® 系统上,这个断言在编译时失败。下面是日志:

g++ 1.cpp --std=c++0x 
1.cpp :1:1: error: static assertion failed: " Integer sizes expected to be 4"

可以在全局范围以及在名称空间、函数体或类声明内使用静态断言。在 清单 2 中,使用静态断言有助于防止用某些参数类型实例化一个模板化的类。方法是添加一个在特殊化的类版本中一定会失败的断言。一定要记住,在使用静态断言时,检查的表达式在编译时必须是可计算的。

清单 2. 在类声明内使用静态断言
// using static assertions as part of class declaration 
template <typename T1, typename T2>
class T {
	T1 a;
	T2 b;
};

template <typename T1>
class T<T1, bool> { 
	static_assert(sizeof(T1) == 0, "T<T1, bool> is not allowed, sorry");
};

int main( )
{ 
   T<float, bool> f1; // this will fail compilation 
   T <float, int> i2; // fine
   …
}

引入新的字符类型

坦白地说,C++ 对 Unicode 的支持一直不够。Unicode 用三种不同的大小定义字符编码 — UTF-8、UTF-16 和 UTF-32 — 而传统的字符类型是 8 位的。使用 wchar_t 是不可行的,因为 wchar_t 的大小由实现决定。

C++0x 引入了两个新的关键字 — char16_tchar32_t,它们具有确定的大小(分别是 16 和 32 位,见 清单 3),都是 unsigned 型的。要想使用这些新类型,必须包含 C++ 头文件 cstdint,尽管标准要求包含头文件 cuchar

清单 3. 使用 char16_t 和 char32_t 数据类型
#include <iostream>
#include <cstdint>  // this is the header for char16_t and char32_t 
using namespace std;

int main()
{
    char16_t c = 'A';
    cout << sizeof(c) << endl;
    char32_t d = 'b';
    cout << sizeof(d) << endl;
    return 0;
}

清单 3 的输出肯定会在控制台上显示 24。现在,试试定义一个 char32_t 类型的数组,见 清单 4

清单 4. 声明并初始化 char32_t 数组
#include <iostream>
#include <cstdintglt;
using namespace std;

int main()
{
    char32_t e[ ] = "Hello World";
    cout << sizeof(e) << endl;
    return 0;
}

下面是编译器日志:

g++ 1.cpp --std=c++0x 
1.cpp: In function 'int main()':
1.cpp:7:8: error: int-array initialized from non-wide string

这里出了什么问题?对于初学者来说,"Hello World" 是每个字符 8 位的常规字符串,把它赋值给每个字符 32 位的数组是非法的。根据类型在字符串前面加上 uU,就可以创建这些新类型的字符串直接值,见 清单 5

清单 5. 初始化 char16_t 和 char32_t 字符串直接值
#include <iostream>
#include <cstdint>  
using namespace std;

int main()
{
    char16_t f[ ] = u"Hello World";  // prefix the string with u for char16_t
    cout << sizeof(f) << endl;
    char32_t e[ ] = U"Hello World"; // prefix the string with U for char32_t
    cout << sizeof(e) << endl;
    return 0; 
}

控制台输出应该是 2448,g++ 可以顺利地编译此代码。


新的 auto 语法:根据初始化器(initializer)表达式推导类型

编译器能够根据变量的初始化器(initializer)表达式推导出正确的变量类型,这个特性可以节省时间。因此,现在不再需要 class1::class3::struct1::enum2 e 这样的声明了。C++0x 为 auto 关键字定义了完全不同的语义。清单 6 给出第一个示例。

清单 6. 使用 auto 进行自动类型推导
#include <iostream>
#include <map>  
using namespace std;

int main()
{
    auto *num1 = new int(7);  // type for num1 is int* 
    const auto num2 = 3.1415;  // type for num2 is double

    // now for some serious business 
    map<int, string> map1;
    map1.insert(make_pair<int, string> (7, "8"));
    auto mapit1 = map1.find(7);  // type for mapit1 is std::map::iterator
    cout << mapit1->second << endl; 
    … // continue coding 
}

num1 的类型是整数指针;num2 是一个浮点值,mapit1std::map::iteratorauto 支持多个声明,但是所有推导的结果必须是相同的类型。按从左到右的次序处理声明。清单 7 给出的代码使用 auto,但是不起作用。

清单 7. 错误地使用 auto 关键字
int main( )
{
   auto i = 9, j = 8.2; // error – i and j should be same type
   auto k = &k; // dumb error – can’t declare and use in initializer
   …
}

下面是 清单 7 的编译器日志:

1.cpp: In function 'int main()':
1.cpp:3:8: error: inconsistent deduction for 'auto': 'int' and then 'double'
1.cpp:4:8: error: variable 'auto k' with 'auto' type used in its own initializer

对于 auto,还有其他一些注意事项。例如,C++0x 不允许使用它作为存储类指示符。如果从 g++ 命令行中删除 –std=c++0x,下面的代码片段可以顺利地编译:

auto int variable1 = 8;

下面是 g++ --std=c++0x 日志:

1.cpp: In function 'int main()':
1.cpp:28:14: error: two or more data types in declaration of 'variable1'

初始化器列表和类型窄化(type narrowing)

还记得传统 C/C++ 中的 int integer_array1 [ ] = {1, 2, 3, 4, 5}; 构造吗?这个列表是使用 {} 定义的,它是初始化器列表。但是,语言并没有为更广泛地使用这个构造提供语义。C++0x 为初始化器列表定义了更多使用规则:

  • 可以在变量定义中使用初始化器列表
  • 可以在 new 表达式中使用初始化器列表
  • 可以用作函数参数和/或函数返回语句
  • 允许作为下标表达式
  • 允许作为构造器调用的参数
  • 不允许类型窄化

在研究初始化器列表示例和典型用法之前,先讨论一下什么是类型窄化。请考虑 清单 8 中的代码。

清单 8. 用浮点值初始化整数数组
int main( )
{
   int nasty[ ] = {8, 99, 2.3, 4.0, 5};
   // … 
   return 0;
}

尽管发生了非常糟糕的 doubleinteger 类型转换,但是当用 g++ –Wall 选项编译以上代码时,编译器不会发出任何警告。这就是类型窄化的示例,好在 C++0x 标准不允许这么做。下面是用 g++ –std=c++0x 编译代码时的日志:

1.cpp: In function 'int main()':
1.cpp:14:34: error: narrowing conversion of 
    '2.29999999999999982236431605997495353221893310547e+0' 
    from 'double' to 'int' inside { }
1.cpp:14:34: error: narrowing conversion of '4.0e+0' 
    from 'double' to 'int' inside { }

现在讨论初始化器列表的用法。现在允许使用 清单 9 中的声明。

清单 9. 对 STL 容器使用初始化器列表
// Initializer list used with variable definition
std::vector<double> doubles = {2.3, 4.511, 1.23, 0.99};

// Initializer list used with new 
std::list<double> *d2 = new std::list<double> {1.2, 1.3}; 

// Initialize a map 
std::map<string, int> = { {“key1”, 1}, {“key2”, 2} };

可以使用初始化器列表对标量变量进行初始化,在这种情况下应用一般的类型窄化规则。如果用空的初始化器列表对变量进行初始化,对象为初始化值。清单 10 的输出是 2 0 a A both x3 and x4 are null

清单 10. 使用初始化器列表进行标量初始化
int main( )
{
    int x{2};
    double x2{};
    char* x3{};
    int* x4 = {};
    char c1 = {‘a’} ;
    char c2 = char{‘A’} ;
    cout << x << " " << x2 << " "  
        << c1 << " " << c2 << endl;
    if (x3 == NULL && x4 == NULL)
        cout << "both x3 and x4 are null\n";
    // int y{2.3}; → don’t try this error due to type narrowing 
    return 0;
}

注意,在 C++0x 中允许 int y(2.3);由于不允许类型窄化,y 等于 2,而 int y{2.3} 是错的。由于采用这种语义,应该尽可能使用初始化器列表。C++0x 标准定义了一个名为 initializer_list 的类类型,可以使用它把参数传递给函数和构造器。另外,函数返回语句可以返回 initializer_list;但是,必须包含头文件 initializer_list 才能使用这个类类型。清单 11 给出 initializer_list 的使用示例。

清单 11. 使用初始化器列表作为函数参数和返回类型
#include <initializer_list> 

// argument to function
void func1(std::initializer_list<int>);

// function returning initializer list
std::initializer_list<double> func2 (double);

清单 12 说明如何访问初始化器列表。注意,函数只能作为不可变的序列访问这个列表 — 也就是说,尝试修改列表的内容会导致错误。

清单 12. 访问初始化器列表的内容
#include <initializer_list> 
using namespace std; 
void display (initializer_list<int> arguments) {
    for (auto p= arguments.begin(); p!= arguments.end(); ++p) {
	    // *p = *p * 2; → Not allowed to modify data 
	    cout << *p << "\n";
    }
}

int main( )
{
    display( {3, 77, 8, 1, 9} );
    return 0;
}

对初始化器列表使用 auto

清单 13 中,x1 的类型是什么?

清单 13. 对初始化器列表使用自动类型推导
int main( )
{
    auto x1 = {2, 4};
    auto z2 = {3, 2.3} ; // go figure
    ...
    return 0 ;
}

x1 的类型是 std::initializer_list<int>。对于 z2,g++ 会抛出错误,指出不允许类型窄化。如果您仍然不相信 x1 的类型,就看看 清单 14

清单 14. 对初始化器列表使用自动类型推导
#include <iostream>
#include <initializer_list>
using namespace std;

template <typename T>
void display() {}

template <>
void display<std::initializer_list<int>> () { cout << "Hurray!\n"; }

int main()
{
   auto x = {2, 3};
   display<decltype(x)> ();
   return 0;
}

清单 14 的输出是 Hurray!display 模板特化结束了所谓的 x1 类型争论。这还引出了另一个 C++0x 构造:decltype


了解 decltype

C++ 没有提供查询变量或表达式的类型的简便方法。GCC 提供一个名为 typeof 的扩展(见 参考资料),但它不是标准的。对于 C++0x,输入 decltype 操作符就会返回变量或表达式的类型。如果您熟悉模板编程,这个特性很可能会促使您马上使用 C++0x。清单 15 对一个变量应用 decltype 操作符,演示对表达式使用这个操作符的方法。

清单 15. 对表达式应用 decltype 操作符
T1 x; 
T2 y;
typedef T3 decltype(x+y); 
T3 z ;

lambda 函数

如果要给 C++0x 的新特性评奖的话,得头奖的应该是 lambda 函数。lambda 函数 是匿名的函数,这意味着不必定义典型的 C/C++ 函数也能够完成工作。lambda 函数最常用的地方可能是 STL sort。到目前为止,要想使用定制的比较函数,标准的做法是定义自己的函数对象,然后适当地定义操作符 ( )。请考虑一个包含 5 个字符串的向量;希望按字符串长度的升序排序。清单 16 给出过去的做法。

清单 16. 过去的排序方法
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;

struct compare {
    bool operator()(const string& s1, const string& s2) { 
        return s1.size() < s2.size();
    }
};

int main()
{
    vector<string> vs = {"This", "is", "a", "C++0x", "exercise"};
    std::sort(vs.begin(), vs.end(), compare());

    for (auto ivs = vs.begin(); ivs != vs.end(); ++ivs)
        cout << *ivs << endl;
    return 0;
}

清单 17 给出使用 lambda 函数的新方法。

清单 17. 定义用于定制排序的 lambda 函数
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;

int main()
{
    vector<string> vs = {"This", "is", "a", "C++0x", "exercise"};
    std::sort(vs.begin(), vs.end(), 
       [ ](const string& s1, const string& s2) { 
          return s1.size() < s2.size(); 
       } 
    )

    for (auto ivs = vs.begin(); ivs != vs.end(); ++ivs)
        cout << *ivs << endl;
    return 0;
}

这两个程序(清单 16清单 17)的输出都是 a is This C++0x exercise清单 18 说明如何定义 lambda 函数。它看起来与定义常规函数的代码很相似,除了 [ ] 部分,对吗?对也不对。在深入讨论之前您必须问几个问题:

  • lambda 函数的返回类型怎么处理?
  • 这些函数可以访问在它的范围之外定义的变量吗?例如,清单 17 中的 lambda 函数是否可以访问 vs

不需要为 lambda 函数提供返回类型;根据返回语句推导出返回类型。在 清单 17 中,lambda 函数的返回类型是 Boolean。对于第二个问题,可以通过 清单 18 中的代码来解答。

清单 18. 在 lambda 函数内访问范围外的变量
int main()
{
    vector<string> vs = {"This", "is", "a", "C++0x", "exercise"};
    std::sort(vs.begin(), vs.end(), 
       [ ](const string& s1, const string& s2) { 
          cout << vs.size( ) << endl;
          return s1.size() < s2.size(); 
       } 
    )
   …
}

每当 sort 调用这个 lambda 函数时,试图输出向量的大小。下面是编译时 g++ 报告的消息:

1.cpp: In lambda function:
1.cpp:12:9: error: 'vs' is not captured

“纠正” 这个问题有两种方法 — 实际上是四种方法。可以使用前面的方括号 ([]) 把参数传递给 lambda 函数。如果选择通过引用传递变量,就在它前面加上 & 并把它放在 [ ] 内;对于通过值传递的变量,在它前面加上等号 (=)。清单 19 通过引用传递 vs

清单 19. 通过引用把变量传递给 lambda 函数
int main()
{
    vector<string> vs = {"This", "is", "a", "C++0x", "exercise"};
    std::sort(vs.begin(), vs.end(), 
       [ &vs ](const string& s1, const string& s2) { 
          cout << vs.size( ) << endl;
          return s1.size() < s2.size(); 
       } 
    )
   …
}

现在,编译器会顺利地完成编译。(请用 [ =vs ] 试试。)那么,把向量传递给 lambda 函数的第三种方法是什么?使用 [ & ]。这种语法表示把所有局部变量通过引用传递给 lambda 函数。从技术上说,还可以使用 [ = ] 把所有局部变量通过值传递给函数,这是第四种方法。但是,由于性能的原因,不推荐这种方法。


可变参数模板

如何定义具有数量可变的参数(每个参数的类型可能不同)的模板化类或函数?C 支持使用 va_list 定义具有数量可变的参数的函数,C++ 在这方面没有改进。直到现在,一直是这样。C++0x 允许定义具有数量可变的参数的函数或类,现在 GCC 提供相同的支持。清单 20 给出语法。

清单 20. 可变参数函数和类模板
template<typename... Types> 
void f(Types... args) // variable number of function arguments 
{
}

template<typename... Types> 
class c // class with 
{
   // member code 
};

// Usages
f(‘a’, “hello”, 2, 3.1); 
class c<int, double, std::vector<string>> c1;

可以使用 typename… 声明可变参数模板。C++0x 还定义了 sizeof… 操作符,它显示参数数量。不幸的是,无法直接遍历参数。探索模板的惟一方法是定义同样的递归版本以及基本案例。这并不理想,但是目前只能这样做。清单 21 演示如何输出可变参数函数模板的参数。

清单 21. 显示可变参数函数模板的内容
void f() { }

template<typename T, typename... Types> 
void f(T a, Types... args)
{
    cout << sizeof...(args) << endl;
    cout << a << endl;
    f(args...);
}

int main()
{
   f("hello", 'a', 8.333);
   return 0;
}

下面是 清单 21 中发生的情况:

  1. 首先调用 f 的模板化版本,第一个参数的类型是字符串,作为大小可变的参数提供字符和双精度值。
  2. 对于第一次调用,sizeof…(args) 显示 2,因为可变的 args 列表中只有两个参数。
  3. 观察语法 args… 现在在 args 的右边。对 f 的第二次调用的可变列表只包含 8.333,而 a 变成第一个参数。
  4. 当处理完所有参数时,g++ 调用 void f() { },这是递归的基线条件。

下面是 清单 21 的输出:

2
hello
1
a
0
8.333

未来的改进

未来的 GCC 版本可能会更让人兴奋。可能会出现的新特性包括:

  • 垃圾收集应用程序二进制接口 (ABI) 支持。应用程序开发人员可以通过 C++0x 标准定义的接口调整垃圾收集过程。例如,预定义函数 void declare_reachable(void* p); 要求垃圾收集器不回收指针 p 指向的存储。
  • 更好地支持并发。GCC 对 C++0x 线程的支持并不适用于所有平台(例如 Cygwin)。正在开发与线程相关的存储。

结束语

对于 C++(或者应该说 C++0x)开发人员,这是一个让人兴奋的时期。标准委员会添加的特性可以改进 C++ 的模板编程、类型安全性、系统软件和库开发(借用 Stroustrup 的话 [见 参考资料])。本文简要介绍了现在在 GCC 4.5 中支持的几个语言特性。更多信息请参见 GCC 网站和 参考资料

参考资料

学习

  • C++0x FAQ:访问由 Stroustrup 维护的 C++0x FAQ 站点。
  • C++0x compiler support:通过对 C++0x 编译器支持的对比研究了解更多信息。
  • C++0x draft standard:阅读 C++0x 标准的草案。
  • C++ papers:阅读 C++ 标准委员会文件。
  • GCC:访问 GCC 主页。
  • typeof:进一步了解 GCC typeof 操作符。
  • Delegating constructors:阅读关于委托构造器的白皮书。
  • AIX and UNIX 专区:developerWorks 的“AIX and UNIX 专区”提供了大量与 AIX 系统管理的所有方面相关的信息,您可以利用它们来扩展自己的 UNIX 技能。
  • AIX and UNIX 新手入门:访问“AIX and UNIX 新手入门”页面可了解更多关于 AIX 和 UNIX 的内容。
  • AIX and UNIX 专题汇总:AIX and UNIX 专区已经为您推出了很多的技术专题,为您总结了很多热门的知识点。我们在后面还会继续推出很多相关的热门专题给您,为了方便您的访问,我们在这里为您把本专区的所有专题进行汇总,让您更方便的找到您需要的内容。
  • AIX and UNIX 下载中心:在这里你可以下载到可以运行在 AIX 或者是 UNIX 系统上的 IBM 服务器软件以及工具,让您可以提前免费试用他们的强大功能。
  • IBM Systems Magazine for AIX 中文版:本杂志的内容更加关注于趋势和企业级架构应用方面的内容,同时对于新兴的技术、产品、应用方式等也有很深入的探讨。IBM Systems Magazine 的内容都是由十分资深的业内人士撰写的,包括 IBM 的合作伙伴、IBM 的主机工程师以及高级管理人员。所以,从这些内容中,您可以了解到更高层次的应用理念,让您在选择和应用 IBM 系统时有一个更好的认识。
  • 技术书店:阅读关于这些和其他技术主题的图书。

获得产品和技术

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=AIX and UNIX
ArticleID=660229
ArticleTitle=GCC 4.5 中的 C++0x 特性支持
publish-date=05232011