c++11新特性介绍

更多关于STL文章——STL学习笔记

c++11 介绍

维基百科)C++11,先前被称作C++0x,即ISO/IEC 14882:2011,是C++编程语言的一个标准。它取代第二版标准ISO/IEC 14882:2003(第一版ISO/IEC 14882:1998公开于1998年,第二版于2003年更新,分别通称C++98以及C++03,两者差异很小),且已被C++14取代。相比于C++03,C++11标准包含核心语言的新机能,而且扩展C++标准程序库,并入了大部分的C++ Technical Report 1程序库(数学的特殊函数除外)。 ISO/IEC JTC1/SC22/WG21 C++标准委员会计划在2010年8月之前完成对最终委员会草案的投票,以及于2011年3月召开的标准会议完成国际标准的最终草案。然而,WG21预期ISO将要花费六个月到一年的时间才能正式发布新的C++标准。为了能够如期完成,委员会决定致力于直至2006年为止的提案,忽略新的提案[1]。最终于2011年8月12日公布,并于2011年9月出版。

核心语言的运行期表现强化

右值引用和move语义

维基百科)在C++03及之前的标准,临时对象(称为右值"R-values",因为它们通常位于赋值运算符右侧)无法被改变,在C中亦同(且被视为等同于const T&)。尽管如此,在某些情况下临时对象仍然可能会被改变,但这种表现也被视为是一个有用的漏洞。

C++11增加一个新的非常量引用(reference)类型,称作右值引用(R-value reference),标记为T &&。右值引用所绑定的临时对象可以在该临时对象被初始化之后做修改,这是为了允许move语义。

C++03低性能问题之一,就是在以传值方式传递对象时隐式发生的耗时且不必要的深度拷贝。举例而言,std::vector本质上是一个C-style数组及其大小的封装,如果一个std::vector的临时对象是在函数内部或者函数回返时创建,要将其存储就只能透过生成新的std::vector并且把该临时对象所有的数据复制过去(为了讨论上的方便,这里忽略回返值优化)。然后该临时对象会被析构,其使用的内存会被释放。

在C++11,把一个vector的右值引用作为参数std::vector的"move构造函数",可以把右值参数所绑定的vector内部的指向C-style数组的指针复制给新的vector,然后把该指针置null。由于临时变量不会被再次使用,所以不会有代码去访问该null指针;又因为该指针为null,当该临时对象超出作用域时曾经指向的内部C-style数组所使用的内存不会被释放。因此,该操作不仅无形中免去了深拷贝的开销,而且还很安全。

右值引用作为数据类型的引入,使得函数可以重载区分它的参数是值类型、传统的左值引用还是右值引用。这让除了标准库的现有代码无须任何改动就能等到性能提升。一个回返std::vector的函数的回返类型无须为了调用move构造函数而显式修改为std::vector&&,因为临时对象自动作为右值。(但是,如果std::vector是没有move构造函数的C++03版,由于传统的左值引用也可以绑定到临时对象上,因此具有const std::vector&参数的复制构造函数会被调用,导致一次显著的内存分配。)

出于安全的考虑,推行了一些限制。具名的变量被认定为左值,即使它是被声明为右值引用数据类型;为了获得右值必须使用显式类型转换,如模板函数std::move()。右值引用所绑定的对象应该只在特定情境下被修改,主要用于move构造函数中。

类型 && 引用名 = 右值表达式;

例如:

#include <iostream>
using namespace std;

class A
{
public:
   A(int n){cout<< n*n <<endl;}
};

int main()
{
    auto a = A();//错误
    auto & b = A(10);  //错误
    auto && c = A(10); //正确
}
//输出100

又如:

bool is_r_value(int &&) { return true; }
bool is_r_value(const int &) { return false; }

int main()
{
    int i{3};
    cout<< is_r_value(i) <<endl; // i为具体变量,即使被宣告成右值引用类型,i作为实参表达式也不会被认定是右值表达式。
    //输出0
    cout<< is_r_value(std::move<int&>(i)) <<endl;
    //输出1
}

由于右值引用的语义特性以及对于左值引用(L-value references;regular references)的某些语义修正,右值引用让开发者能够提供函数参数的完美转发(perfect function forwarding)。当与不定长参数模板结合,这项能力允许函数模板能够完美地转送参数给其他接受这些特定参数的函数。最大的用处是转送构造函数参数,创造出能够自动为这些特定参数调用正确构造函数的工厂函数(factory function)。
引入右值引用的主要目的是提高程序运行的效率。有些对象在复制时需要进行深复制,深复制往往非常耗时。合理使用右值引用可以避免没有必要的深复制操作。

关键字 constexpr 泛化的常量表示式

constexpr 可以让表达式核定于编译期,如:

constexpr int sqrt(int && n){ return n*n; }
int main()
{
    int a[sqrt(9)] = {1,2,3,4};   //正确 a有81个元素
    //如果去掉 constexpr 编译器将报 a大小不明确的错误
}

这个关键字修正了一个在 c++98 使用数值极限时出现的问题。在c++11之前,如下式子

std::numeric_limits::max();

无法被用作一个整数常量,虽然它在功能上等同于宏 INT_MAX。如今,在c++11中这样一个式子被声明为 constexpr,于是我们可以这样应用:

   array<int,std::numeric_limits<int>::max()> arr;

用constexpr修饰函数将限制函数的行为。首先,该函数的回返值类型不能为void。第二,函数的内容必须依照"return expr"的形式。第三,在参数替换后,expr必须是个常量表示式。这些常量表示式只能够调用其他被定义为constexpr的函数,或是其他常量表示式的数据参数。最后,有着这样修饰符的函数直到在该编译单元内被定义之前是不能够被调用的。
声明为constexpr的函数也可以像其他函数一样用于常量表达式以外的地方,此时不需要满足后两点。

核心语言使用性的加强

template 表达式内的空格

c++11中不再要求两个 template 表达式的闭符之间放一个空格

    vector<map<string,int> > phone; //所有标准均正确
    vector<map<string,int>> phone;//从c++11标准开始正确

nullptr 和 std::nullptr_t

c++11 允许使用 nullptr 取代 0 或 NULL,用来表示一个 pointer (指针)指向所谓的 no value(此不同于拥有一个不确定的值)。这个新特性特别能帮助你在“null pointer 被解释为一个整数值”时避免误解。

#include <iostream>
using namespace std;

void fun(int){cout<<"Type int is called!"<<endl;}
void fun(void*){cout<<"Type void* is called!"<<endl;}
int main()
{
   fun(0);
   fun(NULL);//Qt Creator 提示 call to 'fun' is ambiguous
   //匹配异常,NULL无法区分两个重载的fun函数 ,有歧义。
   fun(nullptr);
}

程序运行结果:

Type int is called!
Type void* is called!

nullptr 是个新关键字。它被自动转换为各种pointer 类型,但不会被转换为任何整数类型。它拥有类型 std::nullptr_t ,定义于<cstddef>,所以你现在甚至可以重载函数令它们接受null pointer。注意,std::nullptr_t 被视为一个基础类型。

typedef decltype(nullptr) nullptr_t;

一致初始化与初值列(Initializer List)

在c++11之前,初始化可因为小括号、大括号或赋值操作符(assignment operator)的出现而发生。如何初始化一个变量或对象,很容易混淆。
为此c++11引入了“一致初始化”(Uniform Initialization)概念没,意思是面对任何初始化动作,你可以使用相同语法,也就是使用大括号
以下皆成立:

int i; //i 有未定义值
int j{}; //j被初始化为0
int *p; //p有未定义值
int * q{}  //q初始化为 nullptr

然而请注意,窄化(narrowing)——也就是精度降低或造成数值变动——对大括号而言是不可成立的。例如:

    int x1(5.3); //正确,5.3被窄化为5
    int x2 = 5.3; //正确,5.3被窄化为5
    int x3{5.3};//错误 double不能窄化为int
    int x4 = {5.3};//错误 double不能窄化为int
    char c1{7};//正确 7为int,没有窄化
    char c2{666666}; //错误,666666超过char最大值,窄化
    std::vector<int> v1 {1,2,3,4};//正确
    std::vector<int> v2 {1,2,3.3,4.6};//错误,double不能窄化为int

Bjarne Stroustrup 在 [Stroustrup:FAQ] 对此例的说明:“判定是否窄化转换时,c++11 用以避免许多不兼容性的做法是,依赖初值设定(initializer)的实际值(如上例的7)而非只是依赖类型。如果一个值可被标的类型精确表述,其间的转换就不算转化。浮点数转换至整数,永远是一种窄化——即使是7.0转为7。”

为了支持“用户自定义类型之初值列”概念,c++11 提供了class template std::initializer_list<>,用来支持以一系列值进行初始化,或在“你想要处理一系列值”的任何地点进行初始化。例如:

#include <iostream>
using namespace std;

void print(std::initializer_list<int> vals)
{
    for (auto p=vals.begin();p!=vals.end();++p) {
        cout<<*p<<" ";
    }
    cout<<endl;
}

int main()
{
    print({1,2,3,4,5,6,7,8,9,10});
    //输出1,2,3,4,5,6,7,8,9,10
}

指明实参个数指明一个初值列的构造函数同时存在,带有初值列的那个版本胜出:

#include <iostream>
using namespace std;

class P
{
public:
    P(int,int);
    P(std::initializer_list<int>);
};

int main()
{
    P a(1,2); //调用P(int,int)
    P b{1,2}; //调用P(initializer_list)
    P c{1,2,3};//调用P(initializer_list)
    P d = {1,2};//调用P(initializer_list)
}

如果上述“带有一个初值列”的构造函数不存在,那么接受两个 int 的那个构造函数会被调用以初始化 b和d ,而 c 的初始化将无效。
由于初值列的关系,explicit 之于“接受一个以上实参”的构造函数也变得关系重大。如今你可以令“多数值自动类型转换”不再起作用,即使初始化以 = 语法进行。

#include <iostream>
using namespace std;

class P
{
public:
    P(int,int);
    explicit P(int,int,int);
};

void fp(const P&){}

int main()
{
    P a(1,2); //正确
    P b{1,2}; //正确
    P c{1,2,3};//正确
    P d = {1,2};//正确
    P e = {1,2,3}; //错误
    
    fp({1,2}); //正确
    fp({1,2,3});//错误
    fp(P{1,2});//正确
    fp(P{1,2,3});//正确
}

同样地,explicit 构造函数如果接受的是个初值列,会失去“初值列带有0个、1个初值”的隐式转换能力。

核心语言能力的提升

Range-Based for循环

c++11 引入了一种崭新的 for 循环形式,可以逐一迭代某个给定的区间、数组、集合(range、array、collection)内的每一个元素。其他编程语言可能称此为 foreach 循环。其一般性语法如下:

for (decl : coll) {
statement
}

其中 decl 是给定之 coll 集合中的每个元素的声明,针对这些元素,给定的 statement 会被执行。例如下面针对传入的初值列中的每个元素,调用给定的语句,于是在标准输出装置 cout 输出元素值:

    for(int i:{1,2,3,4,5,6})
        std::cout<< i << std::endl;

如果要将 vector vec的每个元素 elem乘以 3,可以这么做:

    std::vector<int> vec{1,2,3,4,5};
    for(auto& elem:vec)
        elem *= 3;

这里“声明 elem 为一个 reference”很重要,若不这么做,for循环中的语句会作用在元素的一份 local copy 身上(当然或许有时候你想要这样)
这意味着,为了避免调用每个元素的 copy 构造函数和析构函数,你通常应该声明当前元素为一个 const reference。于是一个用来“打印某集合内所有元素”的泛型函数应该写成这样:

template <typename T>
void print(const T& coll)
{
    for(const auto& elem : coll)
        std::cout << elem << " ";
    std::cout<<std::endl;
}

String Literal 字符串字面常量

Raw string 允许我们定义字符序列,Raw string以 R"( 开头,以 **)"**结尾,可以包含 line break。这可以避免使用转义字符。
例如表示字符串“\n”。寻常字面常量可定义为"\\n" 。也可以定义它为 raw string literal R"(\n)"
要在raw string 内写出) " 可以使用定义符 (delimiter)。因此,一个 raw string 的完整语法是 R" delim (...) delim" ,其中 delim 是个字符序列 ,最多16个基本字符,不可含反斜线、空格和小括号。
如:

    string str = R"nc(a\b\nc()")nc";
    cout<<str<<endl; //输出 a\b\nc()"
//等价于寻常字面常量  string str = "a\b\nc()\"";

定义正则表达式的时候非常有用。

  • 编码的 String Literal

只要使用编码前缀,就可以为string literal 定义一个特殊的字符编码。

  1. u8 定义一个 UTF-8 编码。UTF-8 string literal 以 UTF-8 编定的某个给定字符起头,字符类型为 const char
  2. u 定义一个string literal,带着类型为 char16_t 的字符。
  3. U 定义一个string literal,带着类型为 char32_t 的字符。
  4. L 定义一个 wide string literal,带着类型为 wchar_t 的字符。

Raw string 开头的那个R的前面还可以放置一个编码前缀。

关键字 noexcept

noexcept 该关键字告诉编译器,函数不会发生异常,这有利于编译器对程序做更多的优化。如果在运行时,noexcept函数向外抛出了异常(如果函数内部捕获了异常并完成处理,这种情况不算抛出异常),程序会直接终止,调用 std::terminate() 函数,该函数内部会调用 std::abort() 终止程序。

c++的异常处理是在运行时而不是编译时检测的。为了实现运行时检测,编译器创建额外的代码,然而这会妨碍程序优化。
在实践中,一般两种异常抛出是常用的:

  1. 一个操作或函数可能抛出一个异常
  2. 一个操作或函数不可能抛出任何异常

后面这一种方式在以往的c++版本中常用 throw() 表示,在c++11 已经被 noexcept 代替。

int sqrt(int && x) throw()//C++11之前
{ return x*x;}

int sqrt(int && x) noexcept //自C++11起
{ return x*x;}
  • 有条件的 noexcept

在上述示例中 noexcept 其实是 noexcept(true),表示其所限定的 sqrt函数绝对不发生异常。然而,使用方式可以更加灵活,表明在一定条件下不发生异常。

int sqrt(int && n) noexcept(noexcept(n*n))
{ return n*n; }

它表示如果 n*n 不发生异常,那么函数sqrt(int && n)一定不发生异常。

  • 什么时候该使用 noexcept

使用noexcept表明函数或操作不会发生异常,会给编译器更大的优化空间。然而并不是加上它就能提高效率。
以下情形鼓励使用:

  1. 移动构造函数
  2. 移动分配函数
  3. 析构函数(编译器默认添加)
  4. 叶子函数。叶子函数是指在函数内部不分配栈空间,也不调用其他函数,也不存储非易失性寄存器,也不处理异常。

没有把握的情况下,不要轻易使用 noexcept 。

关键字 decltype

decltype 可让编译器找出表达式类型。

std::map<std::string,float> coll;
decltype (coll)::value_type elem;

decltype 的应用之一是声明返回类型,另一个用途是在metaprogramming 或用来传递一个 lambda 类型。

关键字 auto

auto 用来自动推导变量类型。

    auto i = 10; //int
    auto j = 10.0; //double
    std::vector<int> coll{1,2,3};
    auto it = coll.begin(); //vector<int>::iterator
    auto f = [=](){cout<<"hello"<<endl;};
    f();

崭新的 template

  • Variadic Template

自c++11起,template 可拥有那种“得以接受个数不定之template实参”的参数。此能力称为variadic template。

如,一个可以打印元素的不定参print:

void print(){}
template<typename T,typename ...Types>
void print(const T& firstArg,const Types&... args)
{
    for(auto i:firstArg) 
        std::cout<<i<<" ";
    std::cout<<std::endl;
    print(args...);
}

如果传入 1或多个参数,上述的 function template就会被调用,它会把第一实参区分开来,允许第一实参被打印,然后递归调用 print()并传入其余实参。你必须提供一个non-template 重载函数print(),才能结束整个递归动作。

  • Alias Template

自 c++11 起,支持 template (partial) type definition。然而由于关键字 typename 用于此处总是出于各种原因失败,所以引入关键字using,并因此引入一个新术语 alias template。

template<typename T>
using Vec  = std::vector<T>;
Vec<int> coll;

等价于

std::vector<int> coll;

Lambda 表达式

lambda表达式可以当作inline函数使用,常用于for_each等以函数为参数的算法中。
如:
输出1000以内的平方数

#include <iostream>
#include <algorithm>
#include <vector>
#include <cmath>
using namespace std;

int main()
{
    vector<int> coll;
    for(int i = 1;i<1000;++i)
        coll.push_back(i);
    
    for_each(coll.begin(),coll.end(),[=](int elem){
        int a = static_cast<int>(pow(elem,0.5));
        if(a*a == elem)
            cout<<elem<<" ";
    });
    cout<<endl;
}

输出结果为:

1 4 9 16 25 36 49 64 81 100 121 144 169 196 225 256 289 324 361 400 441 484 529 576 625 676 729 784 841 900 961

[](int x, int y) { return x + y; }

这个不具名函数的回返类型是decltype(x+y)。只有在lambda函数符合"return expression"的形式下,它的回返类型才能被忽略。在前述的情况下,lambda函数仅能为一个述句。

在一个更为复杂的例子中,回返类型可以被明确的指定如下:

[](int x, int y) -> int { int z = x + y; return z + x; }

定义在与lambda函数相同作用域的参数引用也可以被使用。这种的参数集合一般被称作closure(闭包)。

[] // 沒有定义任何变量。使用未定义变量会引发错误。
[x, &y] // x以传值方式传入(默认),y以引用方式传入。
[&] // 任何被使用到的外部变量都隐式地以引用方式加以引用。
[=] // 任何被使用到的外部变量都隐式地以传值方式加以引用。
[&, x] // x显式地以传值方式加以引用。其余变量以引用方式加以引用。
[=, &z] // z显式地以引用方式加以引用。其余变量以传值方式加以引用。

新的函数声明语法

(维基百科)标准C函数声明语法对于C语言已经足够。演化自C的C++除了C的基础语法外,又扩展额外的语法。然而,当C++变得更为复杂时,它暴露出许多语法上的限制,特别是针对函数模板的声明。下面的示例,不是合法的C++03:

template< typename LHS, typename RHS> 
  Ret AddingFunc(const LHS &lhs, const RHS &rhs) {return lhs + rhs;} //Ret的型別必须是(lhs+rhs)的型別

Ret的类型由LHS与RHS相加之后的结果的类型来决定。即使使用C++11新加入的decltype来声明AddingFunc的回返类型,依然不可行。

template< typename LHS, typename RHS> 
  decltype(lhs+rhs) AddingFunc(const LHS &lhs, const RHS &rhs) {return lhs + rhs;} //不合法的C++11

不合法的原因在于lhs及rhs在定义前就出现了。直到剖析器解析到函数原型的后半部,lhs与rhs才是有意义的。

针对此问题,C++11引进一种新的函数定义与声明的语法:

template< typename LHS, typename RHS> 
  auto AddingFunc(const LHS &lhs, const RHS &rhs) -> decltype(lhs+rhs) {return lhs + rhs;}

这种语法也能套用到一般的函数定义与声明:

struct SomeStruct
{
  auto FuncName(int x, int y) -> int;
};

auto SomeStruct::FuncName(int x, int y) -> int
{
  return x + y;
}

关键字auto的使用与其在自动类型推导代表不同的意义。

强类型枚举

c++11 允许我们定义强类型枚举,例如:

enum class direction : int
{
    center,
    right,
    left,
    up,
    down
};

只需要在 enum 后指明关键字 class。强类型枚举有如下优点:

  • 不会隐式转换至 int
  • 如果数值(例如 left )不在enum被声明的作用域内,必须写为 direction::left
  • 你可以明显定义底层类型(默认为int)并因此获得一个保证大小。
  • 提前声明 enumeration type 是可能的,那会消除“为了新的 enumeration type 而重新编译”的必要——如果只有类型被使用的话

iota 函数

iota 函数可将给定区间的值设定为从某值开始的连续值,例如将连续十个整数设定为从 1 开始的连续整数(即 1、2、3、4、5、6、7、8、9、10)。

#include <iostream>
#include <numeric>
#include <array>
using namespace std;

int main()
{
    array<int,1000> arr{};
    iota(arr.begin(),arr.end(),1);

    for(auto i:arr)
        cout<<i<<" ";
    cout<<endl;
}

输出结果为:1到1000

参考资料

  • wikipedia
  • The C++ Standard Library (Nicolai M. Josuttis 著)(侯捷译)
原创文章 17 获赞 69 访问量 2万+

猜你喜欢

转载自blog.csdn.net/weixin_45826022/article/details/102944265