【深入理解C++11【5】】
1、原子操作与C++11原子类型
C++98 中的原子操作、mutex、pthread:
#include<pthread.h>
#include <iostream>
using namespace std;
static long long total = ;
pthread_mutex_t m = PHTREAD_MUTEX_INITIALIZER; void* func(void*){
long long i;
for(i=;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 ;
}
C++11中引入 了 std::thread对,以及 atomic_llong原子类型。使得程序更加简练。
#include<atomic>
#include<thread>
#include<iostream>
using namespace std;
atomic_llong total{}; // 原子数据类型
void func(int){
for(long long i = ;i < 100000000LL; ++i){
total += i;
}
} int main(){
thread t1(func1, );
thread t2(func2, );
t1.join();
t2.join();
count<<total<<endl;
return ;
}
<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 = ; // 等同于 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{};
atomic<int> b{}; int ValueSet (int){
int t = ;
a.store(t, memory_order_relaxed);
b.store(, memory_order_relaxed);
} int Observer(int){
cout<<a<<b<<endl;
} int main()
{
thread t1(ValueSet, );
thread t2(Observer, );
t1.join();
t2.join();
cout<<a<<b<<endl;
return ;
}
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{};
atomic<int> b{}; int Thread1 (int){
int t = ;
a.store(t, memory_order_relaxed);
b.store(, memory_order_release); // 本原子操作前所有的写原子操作必须完成。
} int Thread2(int){
while(b.load(memory_order_acquire)!=) // 本原子操作完成才能执行之后所有的读原子操作
cout<<a.load(memory_order_relaxed)<<endl; //
} int main()
{
thread t1(Thread1, );
thread t2(Thread2, );
t1.join();
t2.join();
return ;
}
顺序一致、松散、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();
}
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();
}
上面代码,变量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();
f(NULL);
f((char*));
}
// 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 ;
} // 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(); // 推导出 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*);
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();
Func('a'); // 编译失败 ConvType ci();
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();
Func('a'); // 陶然式转换,编译通过 ConvType ci();
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 ;
}
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 = ;
int b = ;
[=]{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 = , boys = ;
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 ;
}
按值传递方式捕捉的变量是lambda函数中不可更改的常量。
12、lambda 与 STL
使用STL,代码量大,需要学习仿函数;而使用STL,代码简单,不需要前置学习。
13、更多的一些关于lambda的讨论
[] 只能捕捉父作用域的自动变量,超出这个范围就不能捕捉,如全局变量。下面的代码,一些严格的编译器会产生编译错误。
int d = ;
int TryCapture(){
auto ill_lambda = [d]{};
}
如果要捕捉全局变量,可以使用仿函数。
14、数据对齐
sizeof() 返回类型大小,offsetof() 返回成员变量的偏移。一个示例如下:
struct HowManyBytes{
char a;
int b;
}; int main()
{
cout<<sizeof(char)<<endl; //
cout<<sizeof(int)<<endl; //
cout<<sizeof(HowManyBytes)<<endl; //
cout<<offsetof(HowManyBytes, a)<<endl; //
cout<<offsetof(HowManyBytes, b)<<endl; //
}
C++ 中每个类型的数据除去长度等属性外,还有一项被隐藏的属性,那就是对齐方式。数据的起始地址必须是对齐地址的倍数。
在有些平台上,硬件无法读取不按照字节对齐的某些类型的数据,这个时候会抛出异常来终止程序。另外,在一些平台上会造成数据读取效率下降。
C++11 中添加了 alignof() 来查看数据的对齐方式,以及 alignas() 来改变类型的对齐方式。
struct alignas() ColorVector
{
double r;
double g;
double b;
double a;
} alignof(ColorVector):
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[]; // 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[] = {};
int length = /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();
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 = ;
for (i = ; i < ; i++){
areas += area()*i;
}
}
windows平台上,使用 __declspec 来定义属性。如字节对齐:
__declspec(extened-decl-modifier)
__declspec(align()) struct Struct32{
int i;
double d;
};
16、C++11 的通用属性。
C++通用属性可以作用于类型、变量、名称、代码块等。
1)作用于声明的通用属性,即可以写在声明的前面,也可以写在声明的标识符之后。
2)作用于整个语句的通用属性,应该写在语句的起始处。
// 情况一,attr1、attr2 均作用于函数 func
[[attr1]] void func [[attr2]] (); // 情况一,同上
[[attr1]] int arra [[attr2]] [];
[[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