c++11 那些事 (1)
花褪残红青杏小,燕子飞时,绿水人家绕。枝上柳绵吹又少,天涯何处无芳草。 墙里秋千墙外道,墙外行人,墙里佳人笑。笑渐不闻声渐悄,多情总被无情恼。 ——《蝶恋花 春景》苏轼
缘起
C++11 为我们带来了很多耳目一新的特性,这些特性能在很大程序上提升我们的开发效率。那么,本系列文章就是介绍C++11 的这些特性的
C++ 向来以复杂著称,据说没有人能真正的精通 C++,这就是像量子力学一样,学过的人千千万,真正能精通的却寥寥无几。本人对 Linux 系统和 C/C++ 实在是爱的深沉,即使其中有刀山火海,也想去探索一番。为了降低理解的复杂性,每次我只会介绍 C++11 中的几个点,意在以小见大,争取用最简练的语言描述最本质的内容,让读者便于理解。
本系列文章主要参考了《深入理解 C++11》 一书,可以认为本系列文章是这本书的书评或注释。
下面,就开始我们的探索之旅吧。
为什么我的眼里常含泪水? 不是因为我对 C++ 爱得深沉,而是我爱的深沉的同时它又无比的复杂...
编译器预定义宏和预定义标识符
C++11 是兼容 C99 的,C99 标准中要求编译器支持一些预定义的宏和预定义标识符。这些宏和标识符通常是以双下划线开头和结尾。
#include <iostream>
void hello_func(void) {
// 当前代码行所在的文件
std::cout << "__FILE__: " << __FILE__ << std::endl;
// 当前代码行所在的行号
std::cout << "__LINE__: " << __LINE__ << std::endl;
// 当前代码行所在的函数名称,其值为字符串
std::cout << "__FUNCTION__: " << __FUNCTION__ << std::endl;
// 同 __FUNCTION__
std::cout << "__func__: " << __func__ << std::endl;
}
int main(void) {
hello_func();
}
// g++ -Wall -g -std=c++11 test.cc
执行 ./a.out 输出如下:
__FILE__: test.cc
__LINE__: 5
__FUNCTION__: hello_func
__func__: hello_func
这些预定义宏和预定义标识符是程序调试时的利器,在使用时能快速的定位到程序出问题的点。
那么宏和标识符有什么区别呢?宏是由编译器中的预处理器进行处理的,在编译时宏已经被展开,而标识符的值是在编译或运行时才能确定的。__func__ 是一个常量标识符,在编译完成时,其值已经确定。可以用以下命令查看宏和标识符的区别:
g++ -E test.cc > file
file 中最后部分的内容如下:
# 2 "test.cc" 2
void hello_func(void) {
std::cout << "__FILE__: " << "test.cc" << std::endl;
std::cout << "__LINE__: " << 5 << std::endl;
std::cout << "__FUNCTION__: " << __FUNCTION__ << std::endl;
std::cout << "__func__: " << __func__ << std::endl;
}
int main(void) {
hello_func();
}
可见,__FILE__、__LINE__都已经被预处理器展开成对应值,__FUCNTION__ 和 __func__ 却没有被展开,可知前两者是预定义宏,后两者是预定义标识符。
需要注意的是,__func__ 和 __FUNCTION__ 通常用在 函数体内。用在全局范围或者当作函数的默认参数是没有意义的。经过测试,非函数体内这两个标识符的值为空。可用如下程序测试:
#include <iostream>
#include <string>
std::string name = __func__;
void hello_func(std::string s = __func__) {
std::cout << "__func__: '" << s << "'" << std::endl;
}
int main(void) {
hello_func();
std::cout << "name: '" << name << "'" << std::endl;
}
_Pragma 操作符
_Pragma 用于向编译器提供额外的信息从而改变编译器的行为。比如数据在内存中的字节对齐方式等。_Pargma 具体支持哪些参数是编译器强关的。_Pragma 是在 c99 标准中引入的,在其之前,已有 #pragma 提供类似的功能,但 _Pragma 的功能更为强大。
_Pragma 操作符实际上是一个预处理器指令,即其由预处理器处理。而 #pragma 是编译器指令,是当预处理阶段完成后,在程序编译时行解析。
在程序编译过程中,当预处理阶段结束时,_Pragma 会被解析并转换成 #pragma 格式。_Pragma 操作符的语法是:
_Pragma("once")
其参数是一个字符串。预处理器在解析 _Pragma 指令时,会执行以下操作:
- 将参数字符串中的双反斜线\\ 转化为单个反斜线
- 将参数字符串中用斜线转义的引号 " 中的反线去掉,剩下单纯的引号
- 将转换完的字符串作为 #param 的参数加入到程序中
例如如下语句:
_Pragma ("GCC dependency \"parse.y\"")
会被转换成:
#pragma GCC dependency "parse.y"
既然 _Pragma 最终会被转换成 #pragma,那么,_Pragma 存在的意义是什么呢?_Pragma 相对于 #pragma 的优势在于它本身是 一个操作符。操作符是可以被放到宏定义中的,而 #pragma 却不行,例如:
#define PRAGMA(x) _Pragma(#x)
#define CONCAT(y) PRAGMA(GCC warning #y)
CONCAT(msg1)
// 展开结果
_Pragma("GCC warning \"msg1\"")
// 最终展开为
#pragma GCC warning "msg1"
需要注意的是,_Pragma 的参数需要是一个字符串字面值,上面的宏定义 #define PRAGMA(x) _Pragma(#x)
是合法的,而 #define PRAGMA(x) _Pragma("abc" #x)
是不合法的,预处理器会报错,报错的原因是因为_Pragma 也是由预处理处理,预处理器把括号中的("abc"#x) 整体当作了 _Pragma 的参数,导致了不合法的情形出现。而下面的情形是合法的:
#define CONCAT(x) "abc"#x
断言
断言在程序调试时是一个非常有用的工具,相对于打日志,断言能省掉许多的工作量。
动态断言
最为常见的断言是动态断言,即定义在assert.h 头文件中的 assert 函数,其用法如下:
#include<iostream>
#include<cassert>
char *alloc(int n) {
assert(n > 0); // 正确运行的条件
return new char[n];
}
int main(void) {
alloc(-1);
}
上面程序编译完成执行时输出如下:
a.out: test.cc:5: char* alloc(int): Assertion `n > 0' failed.
Aborted
可以看出,当断言条件不满足时,会报错,并且调用 abort() 退出进程。
动态断言可以帮助我们对程序进行快速的调试和错误定位。但当调试完成需要上线时,为了保证程序的性能,我们通常希望这些断言不再起作用。但是如果将所有断言删除,不但增加工作量,而且在下次调试时还要再加入断言语句,为解决这一问题,C 标准库的 assert.h 中定义了NDEBUG 宏。要使断言失效,只要在使用断言的文件中,在包含 assert.h 之前加入如下宏定义即可:
#define NDEBUG
可以将此宏定义加入到某个头文件中,然后将此头文件包含到其他文件中。从assert.h 中可以看到 NDBUG 的原理:
#ifdef NDEBUG
#define assert(expr) (static_cast<void> 0)
#else
...
#endif
可以看出,当在 assert.h 之前定义了 NDEBUG 时,assert() 将被展开为一条无意义的语句,在编译器进行优化时,这条语句通常会被优化掉,这种效果就类似于删除了所有的断言语句。
预处理断言
预处理断言是指由预处理器处理的指令,当不满足断言条件时,预处理器就会报错退出。通常预处理断言用 #if,#elif, #else,#ifdef,#ifndef ,#endif 等预处理指令配合 #error 来完成。使用方法如下:
#include<iostream>
#define NUM 3
#if NUM < 5
#error "NUM must > 5"
#else
#define K 30
#endif
int main(void) {
}
上例中用 #if 实现了预处理器中的断言,编译时预处理器会断言失败,进而报错退出:
g++ -o test.o -c -g -std=c++11 test.cc
test.cc:6:2: error: #error "NUM must > 5"
#error "NUM must > 5"
上述的预处理断言还经常用于保护某些头文件只被系统或库内部使用,而不被用户直接使用。例如在库文件 b.h 中包含了 a.h,我们不希望用户直接使用 a.h,那么我们可以进行如下的断言设计:
// b.h
#ifndef _B_H_
#define _B_H_
#include "a.h"
....
#endif
// a.h
#ifndef _A_H_
#define _A_H_
#ifndef _B_H_ // 只有在 b.h 中可以包含此文件
#error "you cannot use a.h directly"
#endif
#endif
通过在 a.h 中使用预处理断言,可以有效的避免用户或其他文件使用 a.h,这样就能避免头文件引用错误或宏定义冲突问题。
静态断言
我们上面已经提及,预处理断言在是预处理阶段进行检查,而动态断言是在程序运行时才进行检查,那么在预处理和运行之间的编译阶段,是否能处理断言呢? 当然可以,我们知道,C++ 编译器是支持模板元编程的,编译器有如此强大的功能,支持断言当然不在话下。
已经有了预处理断言和动态断言,为什么还要静态断言呢?这里因为预处理断言主要用于处理宏定义,运用范围有限。而动态断言需要在执行时才能确定,而且必须保证动态断言语句一定能执行到,如果没有执行到,断言成功或失败是无法确认的。例如如下的代码:
#include<assert.h>
enum Features {
C99 = 0x0001,
ExtInt = 0x0002,
SAssert = 0x0004,
NoExcept = 0x0008,
SMAX = 0x0010,
};
void check() {
assert((SMAX-1) == (C99 | ExtInt | SAssert | NoExcept));
}
int main(void) {
check();
}
这里我们希望定义的 Features 枚举中,每个元素占一个比特位,并且用 check() 来检查是否是每个元素占一位。要想这个检查起作用,我们必须在执行路径上调用 check 函数,否则即使断言失败我们也不会发现。如果在程序编译期间,就能对枚举的完备性进行检查,而不必再关心是否会执行到的问题,这能在很大程度上减少出错的概率。
再看如下的例子:
#include<assert.h>
#include<cstring>
template<typename T, typename U>
void bit_copy(T& a, U& b) {
assert(sizeof(a) == sizeof(b));
memcpy(&a, &b, sizeof(a));
}
void exec() {
double d = 5.2;
int i = 8;
bit_copy(d, i);
}
int main(void) {
exec();
}
同样是我们在调用 exec() 时才能得知参数是否合法。实际上,模板在编译期间已经完成了实例化, 这时就已经能判断参数是否合法了,不需等到执行时才去判断了。
为了实现编译时期的静态断言,C++11 中引入了 static_assert。它有两个参数,一个是返回 bool 值的表达式,另一个是当bool 值非真时输出的信息,注意第二个参数必须是字符串字面值。
#include<assert.h>
#include<cstring>
template<typename T, typename U>
void bit_copy(T& a, U& b) {
static_assert(sizeof(a) == sizeof(b), "data size error"); // 静态断言
memcpy(&a, &b, sizeof(a));
}
void exec() {
double d = 5.2;
int i = 8;
bit_copy(d, i);
}
int main(void) {
}
上述代码在编译时会报错,即使错误的代码不会被执行到。断言失败信息如下:
g++ -o test.o -c -g -std=c++11 test.cc
test.cc: In instantiation of 'void bit_copy(T&, U&) [with T = double; U = int]':
test.cc:13:16: required from here
test.cc:6:3: error: static assertion failed: data size error
static_assert(sizeof(a) == sizeof(b), "data size error");
由于静态断言是在编译器进行检查的,静态断言使用的值必须是在编译期可以确定的。看如下代码:
int positive(const int n) {
static_assert(n > 0, "val must > 0");
}
上述代码会编译失败,信息如下:
g++ -o test.o -c -g -std=c++11 test.cc
test.cc: In function 'int positive(int)':
test.cc:4:5: error: non-constant condition for static assertion
static_assert(n > 0, "val must > 0");
^
test.cc:4:5: error: 'n' is not a constant expression
而编译器可以确定的值通常包括宏定义的数值、 sizeof()、全局作用域内的 const 值等。下面的代码是可以编译通过的:
int positive(const int n) {
static_assert(sizeof(n) > 4, "val must > 4");
}
int main(void) {
}
noexcept 运算符
noexcept 是一个运算符,有以下两种形式:
noexcept
noexcept(expression)
第一种形式等同于 noexcept(true)。当 noexcept 用于函数声明时,指定了函数是否会抛出异常。noexcept(true) 表示被修饰的函数不抛出异常,noexcept(false) 表示被修饰的函数会抛出异常。
noexcept 操作符的用法如下:
#include<iostream>
void BlockThrow() noexcept {
throw 1;
}
int main(void) {
try {
BlockThrow();
} catch(...) {
std::cout << "BlockThrow Found throw" << std::endl;
}
}
==== 输出:
terminate called after throwing an instance of 'int'
Aborted
noexcept 操作符的作用是阻止异常的传播。从上面代码可以看出,当使用 noexcept 修饰的函数内部抛出异常时,会立即调用 std::terminate 退出进程,程序的执行并没有进入到 main 函数中的 catch 块中,这样就阻止了异常向上层的传播。
带参数的使用方法:
void* operator new(std::size) noexcept(false);
这个语句表示 new 操作符是可以抛出异常的。
需要注意的一点是,noexcept(expression) 中的expresson 的值是在编译期间进行检查的,在计算expression 后,编译器最终会将expression 的结果转换为 ture 或false,之后再传递给 noexcept(),因而,可以认为,noexcept(expression) 最终是被转换为 noexcept(true) 或 noexcept(false) 之后才作用于函数声明的。
由于expression 值是在编译期间检查的,这一值必须是在编译期间可以确定的,例如 const 类型、定义的宏、函数调用等。编译器内 expression 与bool值的转换规则如下:
当 expression 为如下表达式时,会转换为false,否则转换为true:
1. expression 是一个函数调用,且此函数声明中无 noexcept 声明
2. expression 是一个 throw 表达式
还有其他较为复杂情形下的 expression,这里暂不做说明。
既然 noexcept(expression) 是一个操作符,那么其必然有一个返回值,它的返回值是false 或true,注意这里涉及的是返回值,要与上述expression 转换为bool 值相区分。当其修饰函数声明时,我们通常看不到其返回值,其单独使用时,我们能看到返回值。若 noexcept(expression) 返回true,则表示当此表达式修饰函数时,不允许抛出异常,否则允许抛出异常。因而若某个 noexcept(expression) 返回true,那么当此表达式修饰函数时,相当于使用了 noexcept(true),否则相当于使用了 noexcept(false)。
可以用以下程序验证上述转换:
#include <iostream>
void may_throw();
void may_throw1() noexcept(false);
void no_throw() noexcept;
auto lmay_throw = []{};
auto lmay_throw1 = []() noexcept(false) {};
auto lno_throw = []() noexcept {};
int main() {
std::cout << std::boolalpha
<< "Is may_throw() noexcept? " << noexcept(may_throw()) << '\n'
<< "Is may_throw1() noexcept? " << noexcept(may_throw1()) << '\n'
<< "Is no_throw() noexcept? " << noexcept(no_throw()) << '\n'
<< "Is lmay_throw() noexcept? " << noexcept(lmay_throw()) << '\n'
<< "Is lmay_throw1() noexcept? " << noexcept(lmay_throw1()) << '\n'
<< "Is lno_throw() noexcept? " << noexcept(lno_throw()) << '\n';
}
输出如下:
Is may_throw() noexcept? false
Is may_throw1() noexcept? false
Is no_throw() noexcept? true
Is lmay_throw() noexcept? false
Is lmay_throw1() noexcept? false
Is lno_throw() noexcept? true
需要注意的是,C++11 出于安全考虑,如果一个类的析构函数没有显式的声明 noexcept(expression),那么这一析构函数默认是为 noexcept(true) 的。
本篇就暂时介绍到这里,理解不到位的地方,希望能与大家互相探讨。语言是软件开发的基础,是工具。我们对工具有深入的了解才能在软件开发时得心应手,让我们的思想在代码中自由的驰骋,基于此,我才对最为“基础”的语言特性进行了解读,而不是一味的探讨“高大上”的算法、数据结构、架构等等。
对于语言的好坏,还是不做评价了,否则又是一场血雨腥风。但我还是要说:PHP 是这个世界上最好的语言,没有之一。