c++11新特性

285 阅读11分钟

一、c++11

C++11是曾经被叫做C++0x,是对目前C++语言的扩展和修正,C++11不仅包含核心语言的新机能,而且扩展了C++的标准程序库(STL),并入了大部分的C++ Technical Report 1(TR1)程序库(数学的特殊函数除外)。 C++11包括大量的新特性:包括lambda表达式,类型推导关键字auto、decltype,和模板的大量改进。

二、兼容c99

_STDC_

等预定义宏,用于判断当前编译器对标准库的支持,略

_func_

预定义标识符

Pragma

同#pragma,如_Pragma("once"),优点在于可使用在宏内

变长参数的宏定义及_VA_ARGS_

变长参数宏使用示例:

#define LOG(...) {\
    fprintf(stderr, "%s: line:%d", __FILE__, __LINE__);\
    fprintf(stderr, __VA_ARGS__);\
    fprintf(stderr, "\n");\
}

宽窄字符串连接

加入u8、u、U支持,R()原生字符串支持,mbrtoc16(头文件< cuchar >)等函数支持

long long 整形

标准要求long long可以在不同平台有不同长度,但至少是64位,我们在写常数字面量时,可以使用LL(或ll)表示long long,而ULL(ull、Ull、uLL)表示unsigned long long,如:long long i = -90000LL

__cplisplus宏

c++11内被预定义为201103L,可以检测编译器支持情况

noexcept

表示修饰的函数不会抛出异常,在c++11中如果noexcept修饰的函数抛出了异常,编译器可以调用std::terminate函数来终止程序的运行。

快速初始化成员变量

可以在类内直接初始化成员,如:

class Test
{
private:
    int num = 1;
}

非静态成员的sizeof

如sizeof(People::hand)

final/override

final,用于阻止派生类覆盖特定的虚方法或是阻止一个类成为基类;如果派生类在虚函数声明时使用了override描述符,那么该函数必须是重载的其基类中的同名函数,否则代码将无法通过编译,这样能避免不小心覆盖基类的虚函数。

模版函数的默认模版参数

如template< typename T = int > void fun(){}

外部模版

三、易用性1

继承构造函数:

如果派生类需要使用基类的构造函数,通常需要在构造函数内显示声明,俗称"透传",如:

class A
{
public:
    A(int i){}
};

class B : A
{
public:    
    B(int i)
        : A(i)
    {
        
    } 
};

假设构造函数类型较多,写起来很不方便,可以使用using来简化,如:

class A
{
public:
    A(int i){}
};

class B : A
{
public:    
    using A::A;
};

这里我们通过using A::A的声明,把基类中的构造函数悉数继承到派生类B中。

委派构造函数

对于不同类型的构造函数,可以引用,如:

class Info
{
public:
    Info() { InitRest(); }
    Info(int i) : Info() { type = i; }
    Info(char e) : Info() { name = e; }        
};

std::move

  • 左值:可以取地址的,有名字的就是左值
  • 右值:不能取地址,没有名字的就是右值;右值分为将亡值和纯右值(如非引用返回的函数返回的临时变量值);c++中所有的值必属于左值、将亡值、纯右值三者之一
  • 右值引用:两个引号&&是C++ 11提出的一个新的引用类型,如 T && a = XX;

std::move:强制转化为右值, 详细可参考C++ 11 右值引用以及std::move

注意1: std::move只是将参数转换为右值引用而已,它从来不会移动什么,比如说一个简单的std::move可以实现成这样:

template <typename T>
decltype(auto) move (T&& param)
{
    using ReturnType = remove_reference_t<T>&&;
    return static_cast<ReturnType> (param);
}

真正的移动操作是在移动构造函数或者移动赋值操作符中发生的:

T (T&& rhs);
T& operator= (T&& rhs);

std::move的作用只是为了让调用构造函数的时候告诉编译器去选择移动构造函数,所以只要把

std::string &&rr5 = std::move(str5);
改成std::string rr5 = std::move(str5);

就会真的发生移动操作了。

注意2: 有了右值引用,读者可能会写出下面的代码:

Test&& fun()  
{  
    Test t;  
    return std::move(t);  
}  

即return std::move(local_var) 是否有必要?答案是没有必要,因为编译器能自动进行返回值优化(RVO), 当函数返回一个对象时,编译器会将这个对象看作的一个右值(准确来说是将亡值)。所以无需在fun函数中,将return t写成return std::move(t)

完美转发

  • std::forward就可以保存参数的左值或右值特性。不管是T&&、左值引用、右值引用。
  • std::forward都会按照原来的类型完美转发。
  • 不forward的话,传入某函数里面的参数类型永远是左值引用,永远掉不到右值引用的版本。

显示转换操作符

explicit 略

初始化列表

如:

int a = 3 + 4;
int a = { 3 + 4 }
int a ( 3 + 4 )
int a { 3 + 4 }
int g { 2.0f }  //无法编译通过

如上所示,好处为可防止"收窄"导致的问题

POD类型

略,需要memcpy、memset等,需要考虑内存分布时才需要用到

非受限联合体

用户自定义字面量

用于自定义如RGB()结构体之类的变量,略

内联命名空间

模版别名

c++11内,定义别名已经不再是typedef的专属能力,使用using也可以。如:

using uint = unsigned int;

可以使用is_name判断2个类型是否一致

一般化的SFINEA规则

即模版匹配失败不是错误,表示的是对重载的模版的参数进行展开的时候,如果展开导致了一些类型不匹配,编译器并不会报错,略

四、易用性2

右尖括号 > 的改进

c++98中,如果在实例化模版的时候出现连续的2个>哭号,那么他们之间需要一个空格隔离,以避免发生编译错误。因为会将>>优先解析为右移,c++11中,该限制已经取消。

auto

auto的自动类型推导,用于从初始化表达式中推断出变量的数据类型。

atuo i = 1;
atuo str = "hello world";

decltype

decltype类型说明符生成指定表达式的类型。在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值。

语法为:

decltype(expression)

示例:

int var;
const int&& fx(); 
struct A { double x; }
const A* a = new A();
语句类型注释
decltype(fx());const int &&对左值引用的const int
decltype(var);int变量var的类型
decltype(a->x);double成员访问的类型
decltype((a->x));const double&内部括号导致语句作为表达式而不是成员访问计算。由于a声明为 const指针,因此类型是对const double的引用。
using Type = decltype(var);int定义Type类型为int

与auto类型推导不同,auto是不能带走cv(const volatile)限定符的,decltype是可以带走cv限定符的,但其成员不会继承cv限定符。

追踪返回类型

如下所示:

//错误
template <typename T1, typename T2>
decltype(t1 + t2) sum(const T1& t1, const T2& t2) {
    return t1 + t2;
}

//正确,但不建议这样写
template <typename T1, typename T2>
auto sum(const T1& t1, const T2& t2) {
    return t1 + t2;
}

//正确,建议这样写:追踪返回类型
template <typename T1, typename T2>
auto sum(const T1& t1, const T2 & t2) -> decltype(t1 + t2) {
    return t1 + t2;
}

追踪返回类型的函数和普通函数的声明最大的区别在于返回类型的后置,如:

int func(char* a, int b);

auto func(char* a, int b) -> int;

基于范围的for循环

在C++中for循环可以使用类似java的简化的for循环,可以用于遍历数组,容器,string以及由begin和end函数定义的序列(即有Iterator)。

map<string, int> m{{"a", 1}, {"b", 2}, {"c", 3}};  

//1
for (auto p : m)
{  
    cout<<p.first<<" : "<<p.second<<endl;  
} 

//2
for (auto &p : m)
{  
    cout<<p.first<<" : "<<p.second<<endl;  
} 

int arr[5] = {1, 2, 3, 4, 5};

//3
for(auto &e : arr)
{

}

五、提高类型安全

强类型枚举

在enum加上关键字class,如:

enum class Type {General, Light, Medium, Heavy};

智能指针

  • auto_ptr:控制权可以随便转换,但是只有一个在用,用起来会受到诸多限制
  • unique_ptr:控制权唯一,不能随意转换;注意父子类转换时并不会自动析构
  • shared_ptr:可以直接赋值和调用拷贝构造函数,且不会清空原本的智能指针
  • weak_ptr:与std::shared_ptr最大的差别是在赋值时,不会引起智能指针计数增加

助手类enable_shared_from_this、shared_from_this会返回this的shared_ptr

六、提高性能及操作硬件的能力

常量表达式

const int a = 1;是我们熟知的常量,但这也仅仅是在运行时的常量,我们需要编译时的常量,int arry[a],这样就无法编译通过,虽然a是常量。可以用#define a 1来解决,但c++11中对编译时的常量的回答是constexpr,及常量表达式。上述可修改为:

constexpr int GetConst() { return 1; }

常量表达式函数需要有以下特点:

  • 函数体只有单一的return语句
  • 函数必须有返回值
  • 在使用前必须已经定义
  • return语句,不能使用非常量表达式的函数、全局数据,且必须是一个常量表达式

变长模版:

我们在C++中都用过pair,pair可以使用make_pair构造,构造一个包含两种不同类型的数据的容器。比如,如下代码:

auto p = make_pair(1, "C++ 11"); 

由于在C++11中引入了变长参数模板,所以发明了新的数据类型:tuple,tuple是一个N元组,可以传入1个, 2个甚至多个不同类型的数据

auto t1 = make_tuple(1, 2.0, "C++ 11");  
auto t2 = make_tuple(1, 2.0, "C++ 11", {1, 0, 2}); 

这样就避免了从前的pair中嵌套pair的丑陋做法,使得代码更加整洁,另一个经常见到的例子是Print函数,在C语言中printf可以传入多个参数,在C++11中,我们可以用变长参数模板实现更简洁的Print:

template<typename head, typename... tail>  
void Print(Head head, typename... tail) {  
    cout<< head <<endl;  
    Print(tail...);  
} 

Print中可以传入多个不同种类的参数,如下:

Print(1, 1.0, "C++11"); 

变长模版语法:template<typename... Elements> class tuple;我们在标识符Elements前使用了三个点表示该参数是变长的。

原子操作

std::atomic为C++11封装的原子数据类型。

什么是原子数据类型?

从功能上看,简单地说,原子数据类型不会发生数据竞争,能直接用在多线程中而不必我们用户对其进行添加互斥资源锁的类型。从实现上,大家可以理解为这些原子类型内部自己加了锁。

内存模型,顺序一致性及memory_order:参考Singleton & memory order,略

线程局部存储

TLS(thread local storage),g++在全局或者静态变量的声明中加上关键字__thread,即可将变量声明为TLS变量,每个线程都将拥有独立的变量拷贝,一个线程对变量的读写并不会影响到另一个线程的变量数据。

在c++11里面对TLS作了统一规定,与__thread类似,通过thread_local来修饰,如:

int thread_local err_code;

一旦声明一个变量为thread_local,其值将在线程开始的时候被初始化,而在线程结束时,该值也将不再有效。对于thread_local变量地址取值(&),也只可以获得当前线程中的TLS变量的地址。

快速退出

quick_exit和at_quick_exit,前者用户退出,同exit,但并不会调用析构,后者用于注册退出回调。

七、为改变思考方式而改变

nullptr

nullptr是为了解决原来C++中NULL的二义性问题而引进的一种新的类型,因为NULL实际上代表的是0。

默认函数控制

c++中声明自定义的类,编译器默认生成一些默认函数,包括:

  • 构造函数
  • 拷贝构造函数
  • 拷贝赋值函数(operator=)
  • 移动构造函数
  • 移动拷贝函数
  • 析构函数

此外,还会为以下自定义类型提供全局默认操作符函数:

  • operator,
  • operator &
  • operator &&
  • operator *
  • operator ->
  • operator ->*
  • operator new
  • operator delete

c++中,如果程序员实现了这些函数的自定义版本,则编译器不会再为该类自动生成默认版本。 c++11提供了default,delete控制默认函数的生成,如:

class A
{
private:
    A() = default;
    A(const A &) = delete; 
}

Lambda表达式

lambda表达式可以方便地构造匿名函数,如果你的代码里面存在大量的小函数,而这些函数一般只被调用一次,那么不妨将他们重构成 lambda 表达式。

C++11的lambda表达式规范如下:

表达式序号
[ capture ] ( params ) mutable exception attribute -> ret { body }(1)
[ capture ] ( params ) -> ret { body }(2)
[ capture ] ( params ) { body }(3)
[ capture ] { body }(4)
  • (1) 是完整的 lambda 表达式形式
    • mutable 修饰符说明 lambda 表达式体内的代码可以修改被捕获的变量,并且可以访问被捕获对象的 non-const 方法。
    • exception 说明 lambda 表达式是否抛出异常(noexcept),以及抛出何种异常,类似于void f() throw(X, Y)。
    • attribute 用来声明属性。
  • (2) const 类型的 lambda 表达式,该类型的表达式不能改捕获("capture")列表中的值
  • (3) 省略了返回值类型的 lambda 表达式,但是该 lambda 表达式的返回类型可以按照下列规则推演出来:
    • 如果 lambda 代码块中包含了 return 语句,则该 lambda 表达式的返回类型由 return 语句的返回类型确定。
    • 如果没有 return 语句,则类似 void f(...) 函数。
  • (4) 省略了参数列表,类似于无参函数 f()。

capture 指定了在可见域范围内 lambda 表达式的代码内可见得外部变量的列表,具体解释如下:

  • [a, &b] a变量以值的方式被捕获,b以引用的方式被捕获。
  • [this] 以值的方式捕获 this 指针。
  • [&] 以引用的方式捕获所有的外部自动变量。
  • [=] 以值的方式捕获所有的外部自动变量。
  • [] 不捕获外部的任何变量。

示例:

auto func1 = [](int i) { return i+4; };

vector<int> iv{5, 4, 3, 2, 1};  
int a = 2, b = 1;  

for_each(iv.begin(), iv.end(), [b](int &x){cout<<(x + b)<<endl;});      // (1)  
for_each(iv.begin(), iv.end(), [=](int &x){x *= (a + b);});             // (2)  
for_each(iv.begin(), iv.end(), [=](int &x)->int{return x * (a + b);});  // (3) 

八、融入实际应用

对齐支持

通用属性

unicode支持

  • char16_t、char32_t、u8、u、U等unicode字符类型
  • mbrtoc16等字符串转换函数

原生字符串

R("XXXX")

九、标准库的变更

标准库组件上的升级:

  • 利用新特性实现标准库,如move,decltype等,略

线程支持

  • std::thread
  • std::mutex,std::recursive_mutex等等
  • std::condition_variable和std::condition_variable_any
  • std::lock_guard和std::unique_lock
  • std::packaged_task
  • std::future
  • std::promise
  • std::async

多元组类别

  • std::tuple

散列表

  • std::unordered_set
  • std::unordered_multiset
  • std::unordered_map
  • std::unordered_multimap

正则表达式

  • std::regex
  • std::cmatch

通用智能指针

  • shared_ptr
  • weak_ptr
  • unique_ptr
  • auto_ptr将会被C++标准所废弃

可扩展的随机数功能

  • rand
  • std::uniform_int_distribution
  • std::mt19937

包装引用

  • std::ref

多态函数对象包装器

  • std::function
  • std::plus
  • std::bind

用于元编程的类别属性

  • 对于那些能自行创建或修改本身或其它程序的程序,我们称之为元编程,略

用于计算函数对象回返类型的统一方法

  • 要在编译期决定一个模板仿函数的回返值类别并不容易,特别是当回返值依赖于函数的参数时。
  • std::result_of

iota 函数

  • std::iota

十、参考