Tekkaman

导航

 

深入理解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;
}
View Code

  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;
}
View Code

 

  顺序一致、松散、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

 

posted on 2019-01-09 16:43  Tekkaman  阅读(930)  评论(0编辑  收藏  举报