【深入理解C++11【5】】
1、原子操作与C++11原子类型
C++98 中的原子操作、mutex、pthread:
#include<pthread.h> #include <iostream> using namespace std; static long long total = 0; pthread_mutex_t m = PHTREAD_MUTEX_INITIALIZER; void* func(void*){ long long i; for(i=0;i < 100000000LL; i++){ pthread_mutex_lock(&m); total+=i; pthread_mutex_unlock(&m); } } int main() { pthread_t thread1, thread2; if (pthread_create(&thread1, NULL, &func, NULL)){ throw; } if(pthread_create(&thread2, NULL, &func, NULL)){\ throw; } pthread_join(thread1, NULL); pthread_join(thread2, NULL); cout<<total<<endl; return 0; }
C++11中引入 了 std::thread对,以及 atomic_llong原子类型。使得程序更加简练。
#include<atomic> #include<thread> #include<iostream> using namespace std; atomic_llong total{0}; // 原子数据类型 void func(int){ for(long long i = 0;i < 100000000LL; ++i){ total += i; } } int main(){ thread t1(func1, 0); thread t2(func2, 0); t1.join(); t2.join(); count<<total<<endl; return 0; }
<cstdatomic>中原子顾炎武 和内置类型对应表:
更为普遍地,可以使用 atomic类模板,通过该类模板,可以任意定义出需要的原子类型。
std:: atomic< T> t;
C++11中,不允许对原子类型进行拷贝构造、移动构造,以及使用operator=等。总是默认被删除的。
atomic<float> af{1.2f}; atomic<float> af1{af}; // 编译错误
对原子类型解原子则是完全OK的。
atomic<float> af{1.2f}; float f = af; float f1{af};
原子类型是由编译器来保证针对原子类型数据的操作都是原子操作。原子操作都是平台相关的,C++11中定义出了统一的接口,根据编译选项产生其平台相关的实现。
大多数的原子类型,都可以执行读(load)、写(store)、交换(exchange)。
atomic<int> a; int b = a; // 等同于 b = a.load(); atomic<int> a; a = 1; // 等同于 a.store(1);
atomic_flag 比较特殊,不需要使用load、store等成员函数。通过 test_and_set 以及 clear,可以实现一个 spin lock。
atomic_flag 提供了无锁编程。无锁编程可以最大限度地挖掘并行编程的性能。
2、内存模型,顺序 一致性与 memory_order
编译器或处理器可能会改变代码的执行顺序。默认情况下C++11中的原子类型的变量在线程中总是保持着顺序执行的特性(非原子类型没有必要,因为不需要在线程间同步)。我们称这样的特性为“顺序一致”。
PowerPC、ArmV7 是弱内存模型构架。x86是强顺序内存模型平台。
内存栅栏(memory barrier)sync 对高度流水化的PowerPC处理器的性能影响很大。
memory_order_relaxed,表示使用松散的内存模型,该指令可以任由编译器重排序或者由处理器乱序执行。如下:
#include <thread> #include <atomic> #include <iostream> using namespace std; atomic<int> a{0}; atomic<int> b{0}; int ValueSet (int){ int t = 1; a.store(t, memory_order_relaxed); b.store(2, memory_order_relaxed); } int Observer(int){ cout<<a<<b<<endl; } int main() { thread t1(ValueSet, 0); thread t2(Observer, 0); t1.join(); t2.join(); cout<<a<<b<<endl; return 0; }
C++11 中一共有7种 memory_order的枚举值 :
store可以使用:memory_order_relaxed、memory_order_release、memory_order_seq_cst。
load可以使用:memory_order_relaxed、memory_order_consume、memory_order_acquire。
memory_order_seq_cst 是C++11中的默认值。
memory_order_release、memory_order_acquire常常结合使用,称为release-acquire内存顺序。
#include <thread> #include <atomic> #include <iostream> using namespace std; atomic<int> a{0}; atomic<int> b{0}; int Thread1 (int){ int t = 1; a.store(t, memory_order_relaxed); b.store(2, memory_order_release); // 本原子操作前所有的写原子操作必须完成。 } int Thread2(int){ while(b.load(memory_order_acquire)!=2) // 本原子操作完成才能执行之后所有的读原子操作 cout<<a.load(memory_order_relaxed)<<endl; // 1 } int main() { thread t1(Thread1, 0); thread t2(Thread2, 0); t1.join(); t2.join(); return 0; }
顺序一致、松散、release-acquire、release-consume 通常是最为典型的4种内存顺序。
3、线程局部存储(TLS,thread local storage)
C++98 中各个编译器公司都 自己的TLS标准。 g++/clang++ 中可以看到如下的语法:
__thread int errCode;
C++11对TLS做出了统一的规定。通过 thread_local 来声明 TLS变量。
int thread_local errCode。
线程结束时,TLS变量将不再有效。对TLS变量取值 &,也只可以获得当前线程中的TLS变量的地址值。
4、quick_exit、at_quick_exit
terminate函数内部会调用 abort函数,通常指发生异常退出。
abort会发送 SIGABRT,从而被操作系统干掉。
exit、atexit来源于C。exit()属于正常退出,会调用自动变量的析构函数,以及atexit注册的函数。注册函数的调用次序与注册顺序相反,如:
#include <cstdlib> #include <iostream> using namespace std; void openDevice() { cout<<"device is opened."<<endl; } void resetDeviceStat(){ cout<<"device stat is reset."<<endl; } void closeDevice(){ cout<<"device is closed."<<endl; } int main() { atexit(closeDevice); atexit(resetDeviceStat); openDevice(); exit(0); }
device is opened device stat is reset device is closed
C++11 中引入了 quick_exit 函数。该函数并不执行析构函数,而只是使程序终止。quick_exit 是正常退出。at_quick_exit 也可以注册函数。标准要求编译器至少支持32个注册函数的调用。
#include <cstdlib> #include <iostream> using namespace std; struct A{~A(){cout<<"Des A"<<endl;}} void closeDevice(){cout<<"closed."<<endl;} int main() { A a; at_quick_exit(closeDevice); quick_exit(0); }
上面代码,变量a的析构函数不会被调用。
5、nullptr
C++98中的NULL如下:
#undef NULL #if defined(__cplusplus) #define NULL 0 #else #define NULL ((void*)0) #endif
按上述定义,会有如下的经典问题:
#include<stdio.h> void f(char* c){ printf("invoke f(char*)\n"); } void f(int i ){ printf("invoke f(int)\n"); } int main() { f(0); f(NULL); f((char*)0); } // output // invoke f(int) // invoke f(int) // invoke f(char*)
C++中引入了 nullptr,其类型为 nullptr_t,通过 nullptr_t可以声明其他的指针空值类型。
typedef decltype(nullptr) nullptr_t;
nullptr 可以解决C++98 中的 NULL导致的调用整数的问题。
nullptr 有如下特性:
1)nullptr_t类型数据可以隐匿转换成任意一个指针类型。
2)nullptr_t 不能转换为非指针类型。
3)nullptr_t 不适用于算术运算表达式。
4)nullptr_t 可以运用关系运算符。
int main() { // nullptr 可隐匿转换为 char* char *cp = nullptr; // 不可转换为整形,而任何类型也不能转换为 nullptr_t // 以下代码不能通过编译 // int n1 = nullptr; // int n2 = reinterpret_cast<int>(nullptr); // nullptr 与 nullptr_t 类型变量可以作比较 // 当使用 ==、<=、>=符号比较时返回true nullptr_t nptr; if (nptr==nullptr) cout<<"=="<<endl; else cout<<"!="<<endl; if (nptr<nullptr) cout<<"<"<<endl; else cout<<"!<"<endl; // 不能转换为整形或bool类型,以下代码不能通过编译。 // if (0==nullptr) // if (nullptr); // 不可以算术运算,以下代码不能通过编译 // nullptr += 1; // nullptr * 5; // 以下操作均可以正常进行 sizeof(nullptr); typeid(nullptr); throw(nullptr); return 0; } // output // nullptr_t nptr == nullptr // nullptr_t nptr !< nullptr // terminate called after throwing an instance of 'decltype(nullptr)' // Aborted
当nullptr_t与模板结合使用时,会被当成普通类型,而不是一个指针。
template<typename T> void g(T* t) {} template<typename T> void h(T t) {} int main() { g(nullptr); //编译失败,nullptr不被认为是指针。 g((float*)nullptr); // 推导出 T = float h(0); // 推导出 T = int h(nullptr); // 推导出 T = nullptr_t h((flaot*)nullptr) // 推导出 T = float * }
6、一些关于nullptr规则的讨论。
nullptr类型数据所占用的内存空间大小跟void*相同。
sizeof(nullptr_t) == sizeof(void*)
nullptr 的转换是隐式的,(void*)0 则必须经过类型转换。
int foo() { int* px = (void*)0; int* py = nullptr; }
nullptr_t对象 的地址可以被用户使用,但不能获取 nullptr 的地址,但可以声明 nullptr 的右值引用。
int main() { // 可以取 nullptr_t 对象的地址 nullptr_t my_null; printf("%x\n", &my_null); }
7、类与默认函数
C++98 中一理有了自定义版本的构造函数,就会导致我们定义的类型不再是POD。
class TwoCstor{ public: // TwoCstor不再是POD类型 TwoCstor(){} TwoCstor(int i):data(i){} private: int data; }
C++11中提供了=default功能,指示编译器生成该函数的默认版本。
class TwoCstor{ public: // TwoCstor依然是POD类型 TwoCstor() = default; TwoCstor(int i):data(i){} private: int data; }
C++11中还提供了 =delete功能,提示编译器不生成函数的缺省版本。
class NoCopyCstor{ public: NoCopyCstor() = default; NoCopyCstor(const NoCopyCstor & ) = delete; }
一理缺省版本 delete了,重载该函数也是非法的。
8、=default 与 =delete
=default 修饰的函数为显式缺省(explicit defaulted)函数
=delete 修饰的函数为删除(deleted )函数
=defalut不仅可以用于类型的定义中,也可以用于类的实现中,这样的好处可以用于方便地用于多个版本的管理。
class DefaultedOptr{ public: DefaultedOptr & operator = (const DefaultedOptr &); } inline DefaultedOptr & DefaultedOptr::operator = (const DefaultedOptr &) = default;
有一些非缺省函数,如果如果加上=defalut,则编译器会按照某些标准行为为其生成代码。
=delete 可以避免编译器做一些不必要的隐匿数据类型转换。
class ConvType{ public: ConvType(int i){} ConvType(char c)=delete; // 删除char版本 }; void Func(ConvType ct){} int main() { Func(3); Func('a'); // 编译失败 ConvType ci(3); ConvType cc('a'); // 编译失败 }
编译器发现从 char 构造 ConvType的构造函数被delete了,从而产面上面的编译错误。
加上 explicit 后会更精妙 。
class ConvType{ public: ConvType(int i){} explicit ConvType(char c)=delete; // 删除char版本 }; void Func(ConvType ct){} int main() { Func(3); Func('a'); // 陶然式转换,编译通过 ConvType ci(3); ConvType cc('a'); // 显式转换,编译失败 }
如果将 operator new 删除,则可以禁止 new 该类型对象。
#include <cstddef> class NoHeapAlloc{ public: void* operator new(std::size_t)=delete; } int main() { NoHeapAlloc nha; NoHeapAlloc* pnha = new NoHeapAlloc; // 编译失败 return 1; }
9、C++11 中的 lambda 函数
lambda函数跟普通函数相比,不需要定义函数名,取而代之的多了一对方括号[].。lambda函数的语法定义如下:
[capture](parameters) mutable -> return-type {statement}
可以省略的内容:
1)如果不需要参数传递,则parameters连同()可以一起加省略。
2)默认情况下,lambda函数是一个const函数,mutalbe可能省略。但在使用了 mutalbe时,(parameters)不能为空,即使参数为空。
3)不需要返回值时,return-type可以省略。此外,在返回类型明确的情况下,也可以省略该部分,让编译器自行推导。
综上,在最极端 情况下,最为简陋的lambda函数如下:
[]{}
下面是一些lambda函数的例子。
int main() { [](); int a = 3; int b = 4; [=]{return a+b;}; // 省略了参数列表和返回类型,返回类型被推断为int auto func1 = [&](int c){b=a+c;}; //省略了返回类型,无返回值 auto func2 = [=,&b](int c)->int{return b+=a+c;} // 较完整的lambda函数。 }
[this] 表示值传递的方式近现代史当前的 this 指针。 后句列表不允许 变量重复传递,否则 会导致编译时错误。
[=,a] // 编译错误,这里=已经以值传递的方式捕捉了所有变量,a重复。 [&,&this] // 编译错误,这里&已经以引用传递方式捕捉了所有变量,再捕捉this也是一种重复。
块作用域{} 外也可以定义lambda,此时捕捉列表必须为空。
10、lambda 与仿函数
仿函数(functor)是定义了成员函数 operator() 的类型对象。仿函数是编译器实现lambda的一种方式。lambda是仿函数的一种语法甜点。
局部函数(local function / nested function) 能够访问父作用域的变量。C++中没有局部函数,而是以仿函数/lambda来实现了类似功能。
11、关于lambda的一些问题及有趣的实验。
C++11标准允许lambda转换为函数指针,但不允许函数指针转换为lambda。
int main() { int girls = 3, boys = 4; auto totalChild = [](int x, int y) ->int{return x+y;}; typedef int (*allChild)(int x, int y); typedef int (*oneChild)(int x); allChild p; p = totalChild; oneChild q; q = totalChild; // 编译失败,参数类型不一致. decltype(totalChild) allPeople = totalChild; decltype(totalChild) totalPeople = p; // 指针类型无法转换为 lambda. return 0; }
按值传递方式捕捉的变量是lambda函数中不可更改的常量。
12、lambda 与 STL
使用STL,代码量大,需要学习仿函数;而使用STL,代码简单,不需要前置学习。
13、更多的一些关于lambda的讨论
[] 只能捕捉父作用域的自动变量,超出这个范围就不能捕捉,如全局变量。下面的代码,一些严格的编译器会产生编译错误。
int d = 0; int TryCapture(){ auto ill_lambda = [d]{}; }
如果要捕捉全局变量,可以使用仿函数。
14、数据对齐
sizeof() 返回类型大小,offsetof() 返回成员变量的偏移。一个示例如下:
struct HowManyBytes{ char a; int b; }; int main() { cout<<sizeof(char)<<endl; // 1 cout<<sizeof(int)<<endl; // 4 cout<<sizeof(HowManyBytes)<<endl; // 8 cout<<offsetof(HowManyBytes, a)<<endl; // 0 cout<<offsetof(HowManyBytes, b)<<endl; // 4 }
C++ 中每个类型的数据除去长度等属性外,还有一项被隐藏的属性,那就是对齐方式。数据的起始地址必须是对齐地址的倍数。
在有些平台上,硬件无法读取不按照字节对齐的某些类型的数据,这个时候会抛出异常来终止程序。另外,在一些平台上会造成数据读取效率下降。
C++11 中添加了 alignof() 来查看数据的对齐方式,以及 alignas() 来改变类型的对齐方式。
struct alignas(32) ColorVector { double r; double g; double b; double a; } alignof(ColorVector):32
C++ 中的 alignof()、alignas()。alignof返回值是 std::size_t类型的常量。注意,数组的alignof与元素要求相同,如下:
class InComplete; // alignof 编译错误,类型不完整 struct Completed{}; // alignof 1 int main() { int a; // alignof 4 long long b; // alignof 8 auto& c = b; // alignof 8 char d[1024]; // alignof 1,与元素要求相同 }
alignas 也可以用在一般类型上。
alignas(double) char c; alignas(alignof(double)) char c;
C++98 中使用 __attribute__((__aligned__(8))) 来修改对齐方式。
一般情况下,最大标量类型是 long double,其对齐值可以通过 alignof(std::max_align_t)来查询,这也叫做基本对齐值(fundamental alignment)。
容量固定,但是对齐方式变化的泛型:
template<typename T> class FixedCapacityArray { public: void push_back(T t){} char alignas(T) data[1024] = {0}; int length = 1024/sizeof(T); }
C++11 在STL库中,添加了 std::align 函数来动态地根据指定的对齐方式调整数据块的位置。
void* align(std::size_t alignment, std::size_t size, void*& ptr, std::size_t& space);
C++11 还提供了 aligned_storage、aligned_union,来帮助分配对齐的内存块。
template<std::size_t Len, std;:size_t Align =default-alighment> struct aligned_storage; template<std::size_t Len, class... Types> strcut aligned_union;
下面的代码,可能在老旧处理器上引起崩溃。
int main() { char* pchar = (char*)malloc(100); pchar++; int* pint = (int*)pchar; // 此处 pint 可能指向非对齐地址 printf("%d", *pint); }
15、语言扩展到通用属性。
编译器厂商或组织设计出了一系列的语言扩展(language extension)来扩展语法。这些扩展语法并不存在于C++标准中。
比如 g++,使用 __attribute__ 来声明属性。
__attribute__((attribute-list))
下面的 const属性告诉编译器:本函数返回值只依赖于输入,不会改变任何函数外的数据,因此没有任何副作用。从而编译器可以将其优化为常量。
exter int area(int n) __attribute__((const)) int main() { int i; int areas = 0; for (i = 0; i < 10; i++){ areas += area(3)*i; } }
windows平台上,使用 __declspec 来定义属性。如字节对齐:
__declspec(extened-decl-modifier)
__declspec(align(32)) struct Struct32{ int i; double d; };
16、C++11 的通用属性。
C++通用属性可以作用于类型、变量、名称、代码块等。
1)作用于声明的通用属性,即可以写在声明的前面,也可以写在声明的标识符之后。
2)作用于整个语句的通用属性,应该写在语句的起始处。
// 情况一,attr1、attr2 均作用于函数 func [[attr1]] void func [[attr2]] (); // 情况一,同上 [[attr1]] int arra [[attr2]] [10];
[[attr1]] int func([[attr2]] int i, [[attr3]] int j) { [[attr4]] return i+j; }
C++11 中,只预定义了两个通用属性 [[noreturn]] 、[[carries_dependency]]。
17、预定义的通用属性。
[[noreturn]] 用于标识不会返回的函数。用于标识那些不会将控制流程返回的函数。
void DoSomething1(); void DoSomething2(); [[noreturn]] void Throwaway(){ throw "expection"; // 控制流跳转到异常处理 } void Func() { DoSomething1(); ThrowAway(); DoSomething2(); // 该处不可达。 }
如果无意中写了 [[noreturn]],但写返回了控制流程,则会发生段错误。
[[carries_dependency]] 告诉编译器调用的函数与当前代码无依赖,以避免插入内存栅栏 sync。 截至 2013,汉网有编译器支持 [[carries_dependency]]属性。
18、字符集、编码、Unicode
ASCII使用7个二进制位进行标识 ,总共可以标识 128种不同的字符 。
比较觉的基于 Unicode字符集的编码方式有UTF-8、UTF-16及UTF-32。一般人常常把UTF-16和Unicode混为一谈。
UTF-8,采用1-6字节的变长编码。英文通常使用1字节,且与ASCII兼容。而中文常用3字节表示。UTF-8由于节约存储空间,因此使用得比较广泛。
Windows内部采用了UTF-16,而 MacOS、Linux 等则采用了 UTF-8 编码方式。
GB2312先于Unicode出现。早在20世纪80年代,作为国家标准被颁布使用。2个字节一个中文字符。在大陆、新加坡有广泛使用。
BIG5用于繁体中文。2字节表示产一个字符。在香港、台湾、澳门有着广泛的使用。
19、C++11 中Unicode的支持。
C++98 为了支持宽字符,添加了 wchar_t。标准规定,wchar_t的宽度由编译器实现决定。windows上 wchar_t被实现为16位宽,linux上被实现为32位。如此导wchar_t的代码通常不可移植。
C++11引入了两种新的类型:
1)char16_t
2)char32_t
C++11 还定义了常量字符串前缀:
1)u8,表示 UTF-8
2)u,UTF-16
3)U,UTF-32
wchar_t 的前缀为 L 。加上普通字符串,一起是5种表达。
编译器会自动将其连接起来,比如“a”"b" 会变为"ab"。u"a""b",会成为 "u""ab"。
C++11 中还规定了简明的 Unicode字符引用方式,如 '\u4F60' 表示UTF-16编码的字符,是“你”。'\U'后跟8个十六进度,表示UTF-32。
某些系统只能输出 utf-8。
C++11 为 char16_t、char32_t 分别配备了 u16string、u32string。
utf8_t 可以用来引用utf-8字符串。
20、关于 Unicode 库的支持
C11中,新增了一些编码转换函数。下面代码中,mb是multi-byte的缩写。c16、c32是char16、char32的缩写。rt是convert的缩写。mbstate_t是用于返回转换中的状态信息。
上述代码的使用需要 #include <cuchar>。
C++中引入 了local机制。一个locale有很多facet/interface。codecvt是其中一个facet,提供当前 locale下多字符编码到多种 Unicode字符编码转换。C++标准规定,一共要实现4种这样的codecvt facet。
一个 locale 并不一定支持所有的 codecvt。可以通过 has_facet 来查询 。
locale lc("en_US.UTF-8"); bool res = has_facet<codecvt<wchar_t,char,mbstate_t>>(lc);
21、原生字符串
R"()",原生字符串中转义字符失效。
22