C++ 11:更进一步






4.88/5 (26投票s)
了解 C++11 的一些有趣特性。
引言
本文旨在帮助有经验的 C++ 用户理解和使用 C++ 11 中一些有趣的增强功能。我将讨论一些有趣的 C++ 11 特性,包括它们在 Visual Studio 11 Beta 和 g++ 中的可用性。
背景
需要非常好的 C++ 知识。这里我们只讨论新东西,大部分都是高级内容。如果您不是 C++ 专家,最好稍后再阅读本文。
走近
auto
auto 关键字允许从函数返回值推断变量类型。这样您就不必显式指定类型。例如,这段代码
int foo() { return 5; } int j = foo();
可以写成
int foo() { return 5; } auto j = foo();
编译器会自动将 j 视为 int。
您可能会说,这有什么大不了的。并非如此。猜猜当您有一个 typename::user::object::foodump::longlongyum::iterator
时会发生什么
name::user::object::foodump::longlongyum::iterator foo() { return ... } name::user::object::foodump::longlongyum::iterator j = foo(); for(name::user::object::foodump::longlongyum::iterator x = some.begin() ; x < some.end() ; x++) { ... }
所有这些都很无聊且容易出错。使用 auto 可以大大简化和清理代码。
name::user::object::foodump::longlongyum::iterator foo() { return ... } auto j = foo(); for(auto x = some.begin() ; x < some.end() ; x++) { ... }
我的评价: 非常有帮助。 允许避免拼写错误。
Visual C++ 11:可用。
G++:可用。
constexpr
constexpr 关键字允许您指定一个始终具有常量返回值的函数。考虑这个问题
int foo() { return 15; } int j[foo()];
编译器错误。编译器无法知道 foo() 的返回值是否是常量。
constexpr int foo() { return 15; } int j[foo()];
编译器知道 foo()
将返回一个常量。
我的评价:不确定。 constexpr
函数有很多限制,而且实际上,int j[15]
比 int j[foo()]
更简洁。但是 constexpr 可以作为编译器提示,用编译时计算替换运行时计算 - 尽管我还没有找到实际的例子。
Visual C++ 11:不可用。
G++:可用。
显式覆盖、final、delete 和 default
这些修饰符允许您在成员函数中明确指定
- override 告诉编译器某个函数必须覆盖父类函数。
- final 告诉编译器某个函数不得被子类函数覆盖。
- default 告诉编译器显式生成默认构造函数。
- delete 告诉编译器某个类的成员不得被调用。
如果您想覆盖父函数,但又不小心更改了函数签名,从而生成了第二个函数,那么 override 就很有用。
class a { public: virtual void foo(int) {...} }; class b : public a { public: virtual void foo() override; // Compiler error. void foo() has not replaced void foo(int) }
这里程序员打算用 b::foo()
覆盖 a::foo()
,但如果没有 override 指示,就会创建一个新的虚拟函数,因为程序员忘记了原始的 foo()
需要一个 int 作为参数。
final 对于确保函数永远不会被覆盖很有用。
class a { public: virtual void foo(int) final {...} }; class b : public a { public: virtual void foo(int) {}; // Compiler error. void foo(int) is final. }
default 允许程序员告诉编译器自动生成默认构造函数,即使可能存在其他构造函数。
class a { public: a() = default; a(int) { ... } };
delete 允许程序员显式限制对成员的调用,例如可能由于不必要的类型转换或复制构造函数而被调用的函数。
class a { public: a(const a&) = delete; void x(int) = delete; void x(float); };
在这里,类复制构造函数无法调用,并且调用 a::x()
必须显式传递一个浮点数(例如,您不能调用 x(0)
)。
我的评价:有用。 它有助于避免不必要的错误。
Visual C++ 11:不可用。
extern 模板
模板中的 extern 关键字告诉编译器不要实例化模板。
template <typename X> class a { public: void test(X); }; template class a<int>; // C++ 03 instantiation extern template class a<int> // C++ 11 won't instantiate.
C++ 11 不会实例化 "a",但 a 将对当前的编译单元可见。
我的评价:有用。 它有助于避免在多个 cpp 文件中不必要的实例化。
Visual C++ 11:不可用。
哈希表
C++ 11 具有无序关联容器,称为 **哈希表**。这些容器使用哈希函数来存储元素。
void test_hash() { unordered_map<string,int> days(7); const char* str[] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"}; for(int i = 0 ; i < 7 ; i++) days[str[i]] = i; auto j = days["Wed"]; // j = 3; }
我的评价:如果您需要,则有用。
Visual C++ 11:可用。
G++:可用。
初始化列表
终于,C++ 中缺失了一样东西。能够用 {} 初始化非 POD 对象。例如,这可以工作
int j[5] = {1,2,3,4,5};
但这不行
vector<int> j = {1,2,3,4,5};
以前需要用麻烦的方式赋值,例如使用 for 循环。C++ 11 允许一个特殊的模板 initializer_list
,它可以用 { } 获取值,并且可以作为任何函数(构造函数或非构造函数)的成员参数:模板 <typename X>
类 A
template <typename X> class A { public: A(int x); A(std::initializer_list<int> y); void foo(std::initializer_list<X> list); }; A<int> a(5); // construct using A(int); A<float> b({1,2,3,4,5,6}); // construct using A(std::initializer_list<int> y); b.foo({0.5f,0.6f}); // call foo with parameters.
我的评价:非常有用。
Visual C++ 11:不可用 :-(。
G++:可用。
Lambda 函数
**Lambda 函数** 是一个可以在另一个函数内部使用的匿名函数。
语法:[capture][arguments]{body}(<call parameters>)
capture 是将在函数体(例如,函数)外部使用的标识符列表。任何在 Lambda 函数外部使用标识符的尝试都会产生编译器错误,如果该标识符不在捕获列表中。
vector<int> flist(15); int sum = 0; for(int i= 0 ; i < 15 ;i++) flist[i] = i; for(int i = 0 ; i < 15 ; [&sum](int x) { sum += x;}(flist[i++])); // sum = 105.
[&sum](int x) { sum += x;} 是 lambda 函数。它通过引用捕获外部变量“sum”,并将其加上作为参数传递的“x”。它在 for 循环内被 flist[i] 立即调用。
您不需要立即调用它。
auto f = [] (int x, int y) { return x + y; }; int j = f(5,6); // j = 11;
我的评价:有用,但要小心,因为它很容易导致难以维护的代码。
Visual C++ 11:可用。
G++:可用。
nullptr
nullptr 常量(类型为 nullptr_t)允许您指定一个不能从零隐式转换的指针类型。
考虑这个问题
int foo(int x) { ... } int foo(char* a) { ... } foo(NULL);
调用 foo(NULL)
将调用 foo(int)
,而不是 foo(char*)
,因为 NULL 定义为 0。
int foo(int x) { ... } int foo(char* a) { ... } foo(nullptr);
现在,将调用 foo(char*)
。nullptr 可以隐式转换为任何其他指针类型,但不能转换为任何整数 int 类型(布尔类型除外)。int j = nullptr 会产生编译器错误。
我的评价:几乎无用。 很少会重载同时接受整数类型和指针的函数,即使有,通常也是您的设计存在缺陷。
Visual C++ 11:可用。
G++:可用。
多态函数指针
这些类似于函数指针,但它们允许隐式转换其参数。例如
int foo(int x) { return x + 1;} typedef int (*foop)(short y); foop f = foo; // Error. It is int foo(int), not int foo(short);
使用多态指针可以实现此类转换。
std::function<bool (int)> foo; bool f2(short a) { ... } foo = f2; // Valid. short can be converted to int bool A = foo(5);
我的评价:有用,如果您知道自己在做什么。C++ 是强类型的,削弱这个特性可能会带来麻烦。
Visual C++ 11:不可用。
G++:可用。
正则表达式
您可能从 IRegExp 中了解的函数现在位于标准头文件 <regex> 中。
char* str = "<h2>Header</h2>"; regex rx("<h(.)>([^<]+)"); cmatch res; eegex_search(str, res, rx); // isolate "Header"
当然,正则表达式是一个独立的章节,我们无法在此讨论。但将其视为标准是个好主意。
我的评价:如果您需要正则表达式,则有用。
Visual C++ 11:可用。我敢肯定他们使用的是旧的 VB 的 IRegExp ;)
G++:可用。
sizeof 成员
在 C++ 11 中,**sizeof** 可以用来获取类成员的大小,而无需类实例。
class a { public int b; }; int x = sizeof(a::b); // Valid in C++ 11. In C++ 03 you had to have an instance of a class.
我的评价:必需。
Visual C++ 11:不可用。
static assert
assert 关键字可以在运行时用于测试断言,而 #error 预处理器指令可以在预处理器中使用。新的关键字 static_assert 可用于编译器。
int j = 0; static_assert(j,"error!"); // Compiler error: compilation failed.
我的评价:可能有用,但不太重要。
Visual C++ 11:可用。
G++:可用。
字符串字面量
到目前为止,您可以使用 "string" 和 L"string" 分别使用普通字符串和宽字符串。C++ 11 添加了以下(非常有用)的字面量:
- u8"string" 用于定义 UTF-8 字符串。
- u"string" 用于 UTF-16 字符串。
- U"string" 用于 UTF-32 字符串。
- R"string" 用于原始字符串。
使用 R"string" 可以让您从源复制/粘贴字符串,而无需转义特殊字符,例如 \。
char* a1 = "Hello there, you can use the \ and / characters to pass arguments"; // Ugh, probably a bug, we forgot to escape \ char* a2 = R"Hello there, you can use the \ and / characters to pass arguments"; // Fine.
我的评价:强制,尤其是 R"string"。
Visual C++ 11:不可用。气死我了。
G++:可用。
强枚举
枚举不是类型化的,例如
enum test {a = 0,b,c,d,e}; // implementation of the enum and it's size is compiler-dependant int j = e; // valid. bool h = d; // valid.
现在它们可以被类型化了。
enum class test: unsigned long {a = 0,b,c,d,e}; // implementation of the enum now is with unsigned long; Guaranteed size and type. int j = -1; if (j < test::c) //warning, comparing signed to unsigned.
我的评价:好。
Visual C++ 11:不可用。
G++:可用。
线程
C++ 11 添加了线程类(包括互斥锁等同步机制)。
void test_thread_func(const char* message) { // Do something with parameter } void test_thread() { thread t(bind(test_thread_func,"hi!")); t.join(); }
这将在单独的线程中启动“test_thread_func”,并使用 join() 函数等待其完成。还有许多其他功能,如互斥锁、线程局部存储支持、原子操作等。
我的评价:好,尽管您通常需要特定于操作系统的扩展来实现严肃的线程模型。幸运的是,有一个 native_handle() 成员函数,它返回本地句柄(在 Win32 中是 HANDLE),因此您可以在没有 C++ 替代方案时使用它。
Visual C++ 11:可用。
G++:不可用(至少我没能编译成功)。
Unique_Ptr 和更多智能指针
unique_ptr(C++ 11 新增)、shared_ptr 和 weak_ptr 都可用。unique_ptr 以一种指针可以被移动的方式扩展了(现在已弃用)的 auto_ptr。
unique_ptr<int> p1(new int(100)); unique_ptr<int> p2 = move(p1); // Now p2 owns the pointer. p2.reset(); // memory is freed p1.reset(); // does nothing.
我的评价:好,尽管您不应该过度依赖自动指针来获得好处。使用分配器类来避免 new 和 delete 很好,但不要过度使用,因为在我看来,xxxx_ptr 类已经太多了。不要陷入依赖智能指针来增强设计糟糕的应用程序的陷阱。
此外,请注意,许多人认为使用智能指针可以在发生异常时安全地销毁内存,这样您就可以在没有泄漏的情况下继续。请记住,异常应该是:您 99% 不应正常继续的错误。不要用异常代替 if 或 switch。不要对密码无效、文件未找到等错误抛出异常。不要捕获“内存不足”异常。您的代码已经有问题,或者 Windows 极不稳定!
Visual C++ 11:可用。
G++:可用。
无限制联合
无限制联合允许在什么可以作为联合体以及什么不能作为联合体方面有更大的自由度。
class A { int b; int c; A() { b =5; c = 4;} }; union B { int j; A a; // Invalid in C++ 03: A has a non trivial constructor. Valid in C++ 11: };
我的评价:危险。 联合体主要用于低级函数,如 C 位操作、汇编等。它们**应该**受到严格限制。
Visual C++ 11:不可用。
变量对齐
到目前为止,您可以使用特定于编译器的 #pragmas 来指定对齐。C++ 11 允许使用 alignas 关键字。
alignas(double) int d[8]; // d contains 8 integers, but it's size is enough to contain 8 doubles also.
此外,alignof 运算符返回标识符的对齐方式,例如 int j = alignof(d) 将返回 sizeof(double)。
我的评价:有用。
Visual C++ 11:不可用。
G++:不可用。
可变参数模板
我们文章的最后一项讨论可变参数模板。这些是能够接受任意数量和任意类型参数的模板。
template <typename ...T> void foo(T...args)
这允许类型安全地实现 printf 等函数。下面的示例检查参数的大小,并使用适当的参数调用 func1()。
void func1(int a1, int a2, int a3) { int j = a1 + a2 + a3;} template<class...T> int funcV(A...args) { int size = sizeof...(A); if (size == 0) func1(1,2,3); if (size == 1) func1(1,2,args...); if (size == 2) func1(1,args...); if (size == 3) func1(args...); } int main() { funcV(0); funcV(0,1,2); funcV(5,3); funcV("hello"); // error, func1 does not accept a char*. }
由于“args”不能轻易地在函数中使用(因为它们的类型和大小是未知的),因此它们通常通过递归调用。
我的评价:有用,但它们确实很容易导致难以理解的代码。谨慎使用。
Visual C++ 11:不可用。
G++:不可用。
代码
附带的 C11.cpp 包含 Visual C++ 11 Beta 和 G++ 支持的所有 C++ 11 功能的示例。如果您发现更多支持的功能和/或错误,请通知我!
玩得开心!
历史
- 2012 年 3 月 24 日:添加了 G++ 支持
- 2012 年 3 月 11 日:首次发布