C++11新特性概要分享

2011年c++11标准通过

比c++98标准通过,晚了11年多,140个新特性,约600个缺陷的修正。

1990年,The Annotated C++ Reference Manual,描述了c++核心机制,没有涉及库。

1998年,C++98标准提出,包括STL、locale、iostream、numeric、string。

2003年,C++03标准提出,包含了一个对98标准的技术勘误表。

2005年,提出了可能在新标准中的14个新特性。

2007年,确定了新标准的核心特性。

2008年,新标准草案通过,GCC4.3已经开始支持新标准中的特性。

2010年,新标准基本完成,clang2.8开始支持新标准种的特性。

2011年,新标准最终发布。

C++98/03的设计目标

  • 比C语言更适合系统编程(且与C语言兼容)
  • 支持数据抽象
  • 支持面向对象编程
  • 支持泛型编程

C++11的设计目标

  • 使C++成为更好的适用于系统开发和库开发的语言
  • 使C++更易于教学,语法更加一致化和简单化
  • 保持语言稳定性,与03标准和C语言兼容

相较于98/03标准,11标准的显著增强。

  • Native Concurrency:内存模型,线程,原子操作
  • 统一对泛型编程的支持:统一初始化表达式,auto,decltype,移动语义。
  • 更好的系统编程:constexpr(常量表达式)、POD
  • 更好的库开发:内联命名空间,继承构造函数,右值引用

新关键字

alignas:用于内存对齐,以替代如GCC的__attribute__((__aligned__((#)))),并且更强大

参考博客关于内存对齐的那些事

alignas 指定符可应用到变量或非位域类数据成员的声明,或能应用于 class/struct/union 或枚举的定义。它不能应用于函数参数或 catch 子句的异常参数。

以此声明声明的对象或类型的对齐要求将等于用于声明的所有 alignas 指定符最严格(最大)的非零 expression ,除非这会削弱类型的自然对齐。

若声明上的最严格(最大) alignas 弱于假如它无 alignas 指定符的情况下本应有的对齐(即弱于其原生对齐或弱于同一对象或类型的另一声明上的 alignas ),则程序为病式:

struct alignas(8) S {};
struct alignas(1) U { S s; }; // 错误:若无 alignas(1) 则 U 的对齐将为 8

非法的非零对齐,例如 alignas(3) 为病式。

忽略在同一声明上弱于另一 alignas 的合法的非零对齐。

始终忽略 alignas(0) 。

alignof:获取内存对齐,替代__alignof__

返回 std::size_t 类型值。

返回由类型标识所指示的类型的任何实例所要求的对齐字节数,该类型可以为完整类型、数组类型或者引用类型。

若类型为引用类型,则运算符返回被引用类型的对齐;若类型为数组类型,则返回元素类型的对齐要求。

#include <iostream>
 
struct Foo {
    int   i;
    float f;
    char  c;
};
 
struct Empty {};
 
struct alignas(64) Empty64 {};
 
int main()
{
    std::cout << "Alignment of"  "\n"
        "- char             : " << alignof(char)    << "\n"
        "- pointer          : " << alignof(int*)    << "\n"
        "- class Foo        : " << alignof(Foo)     << "\n"
        "- empty class      : " << alignof(Empty)   << "\n"
        "- alignas(64) Empty: " << alignof(Empty64) << "\n";
}
Alignment of
- char             : 1
- pointer          : 8
- class Foo        : 4
- empty class      : 1
- alignas(64) Empty: 64

decltype:编译时类型推导,以一个普通表达式为参数,返回该表达式类型,并不会对该表达式求值

参考博客C++11特性:decltype关键字

  • 推导出表达式类型:
int i = 4;
decltype(i) a; //推导结果为int。a的类型为int。
  • 与using/typedef合用,用于定义类型:
using size_t = decltype(sizeof(0));//sizeof(a)的返回值为size_t类型
using ptrdiff_t = decltype((int*)0 - (int*)0);
using nullptr_t = decltype(nullptr);

vector<int >vec;
typedef decltype(vec.begin()) vectype;
for (vectype i = vec.begin; i != vec.end(); i++)
{
    //...
}

这样和auto一样,也提高了代码的可读性。

  • 重用匿名类型:

在C++中,我们有时候会遇上一些匿名类型,如:

struct 
{
    int d ;
    doubel b;
}anon_s;

而借助decltype,我们可以重新使用这个匿名的结构体:

decltype(anon_s) as ;//定义了一个上面匿名的结构体,as和anon_s是一个类型
  • 泛型编程中结合auto,用于追踪函数的返回值类型:

这也是decltype最大的用途了。

template <typename _Tx, typename _Ty>
auto multiply(_Tx x, _Ty y)->decltype(_Tx*_Ty)
{
    return x*y;
}

auto(重新定义):变量的自动类型判断,在声明变量的时候根据变量初始值的类型自动为此变量选择匹配的类型。

参考博客C++11特性:auto关键字

auto可以在声明变量的时候根据变量初始值的类型自动为此变量选择匹配的类型,类似的关键字还有decltype。举个例子:

int a = 10;
auto au_a = a;//自动类型推断,au_a为int类型
cout << typeid(au_a).name() << endl;
int

这种用法就类似于C#中的var关键字。auto的自动类型推断发生在编译期,所以使用auto并不会造成程序运行时效率的降低。而是否会造成编译期的时间消耗,我认为是不会的,在未使用auto时,编译器也需要得知右操作数的类型,再与左操作数的类型进行比较,检查是否可以发生相应的转化,是否需要进行隐式类型转换。

上面举的这个例子很简单,在真正编程的时候也不建议这样来使用auto,直接写出变量的类型更加清晰易懂。下面列举auto关键字的正确用法。

  • 用于代替冗长复杂、变量使用范围专一的变量声明。

想象一下在没有auto的时候,我们操作标准库时经常需要这样:

#include<string>
#include<vector>
int main()
{
    std::vector<std::string> vs;
    for (std::vector<std::string>::iterator i = vs.begin(); i != vs.end(); i++)
    {
        //...
    }
}

这样看代码写代码实在烦得很。有人可能会说为何不直接使用using namespace std,这样代码可以短一点。实际上这不是该建议的方法(C++Primer对此有相关叙述)。使用auto能简化代码:

#include<string>
#include<vector>
int main()
{
    std::vector<std::string> vs;
    for (auto i = vs.begin(); i != vs.end(); i++)
    {
        //..
    }
}

for循环中的i将在编译时自动推导其类型,而不用我们显式去定义那长长的一串。

  • 在定义模板函数时,用于声明依赖模板参数的变量类型。
template <typename _Tx,typename _Ty>
void Multiply(_Tx x, _Ty y)
{
    auto v = x*y;
    std::cout << v;
}

若不使用auto变量来声明v,那这个函数就难定义啦,不到编译的时候,谁知道x*y的真正类型是什么呢?

  • 模板函数依赖于模板参数的返回值
template <typename _Tx, typename _Ty>
auto multiply(_Tx x, _Ty y)->decltype(_Tx*_Ty)
{
    return x*y;
}

当模板函数的返回值依赖于模板的参数时,我们依旧无法在编译代码前确定模板参数的类型,故也无从知道返回值的类型,这时我们可以使用auto。格式如上所示。
decltype操作符用于查询表达式的数据类型,也是C++11标准引入的新的运算符,其目的也是解决泛型编程中有些类型由模板参数决定,而难以表示它的问题。
auto在这里的作用也称为返回值占位,它只是为函数返回值占了一个位置,真正的返回值是后面的decltype(_Tx*_Ty)。为何要将返回值后置呢?如果没有后置,则函数声明时为:

decltype(_Tx*_Ty)multiply(_Tx x, _Ty y)

而此时_Tx,_Ty还没声明呢,编译无法通过。

static_assert:静态断言,编译期的断言。

assert是运行期断言,#error勉强算是预编译期断言。参考博客C++11 static_assert

语法:

static_assert(常量表达式,提示字符串)。

如果第一个参数常量表达式的值为false,会产生一条编译错误,错误位置就是该static_assert语句所在行,第二个参数就是错误提示字符串。

简单范例:

static_assert(sizeof(void *) == 4, "64-bit code generation is not supported.");

该static_assert用来确保编译仅在32位的平台上进行,不支持64位的平台,该语句可以放在文件的开头处,这样可以尽早检查,以节省失败情况下的编译时间。

using(重新定义):增加了类型别名,模板别名的语义,效果与typedef相近

参考文档类型别名,别名模版(C++11 起),参考提问[C++ 'typedef' vs. 'using … = …' [duplicate]](https://stackoverflow.com/que...

template<class T>
struct Alloc { };
template<class T>
using Vec = vector<T, Alloc<T>>; // type-id 为<T, Alloc<T>>
Vec<int> v; // Vec<int> 同 vector<int, Alloc<int>>

特化别名模版时生成的类型不允许直接或间接使用其名:

template<class T>
struct A;
template<class T>
using B = typename A<T>::U; // type-id 为 A<T>::U
template<class T>
struct A { typedef B<T> U; };
B<short> b; // 错误: B<short> 通过 A<short>::U 使用其类型

noexcept:进行编译时检查,若表达式声明为不抛出任何异常则返回 true;指定函数是否抛出异常。

参考文档noexcept 运算符 (C++11 起)noexcept 指定符 (C++11 起)

指定函数是否抛出异常。

void f() noexcept; // 函数 f() 不抛出
void (*fp)() noexcept(false); // fp 指向可能抛出的函数
void g(void pfa() noexcept);  // g 接收指向不抛出的函数的指针
// typedef int (*pf)() noexcept; // 错误

它可用于函数模板的 noexcept 指定符以声明函数将对某些类型抛出异常,但不对其他类型抛出。

#include <iostream>
#include <utility>
#include <vector>

void may_throw();
void no_throw() noexcept;
auto lmay_throw = [] {};
auto lno_throw = []() noexcept {};
class T
{
public:
  ~T() {} // 析构函数防止移动构造函数
          // 复制构造函数是 noexcept
};
class U
{
public:
  ~U() {} // 析构函数防止移动构造函数
          // 复制构造函数是 noexcept(false)
  std::vector<int> v;
};
class V
{
public:
  std::vector<int> v;
};

int main()
{
  T t;
  U u;
  V v;

  std::cout << std::boolalpha
            << "Is may_throw() noexcept? " << noexcept(may_throw()) << '\n'
            << "Is no_throw() noexcept? " << noexcept(no_throw()) << '\n'
            << "Is lmay_throw() noexcept? " << noexcept(lmay_throw()) << '\n'
            << "Is lno_throw() noexcept? " << noexcept(lno_throw()) << '\n'
            << "Is ~T() noexcept? " << noexcept(std::declval<T>().~T()) << '\n'
            // note: the following tests also require that ~T() is noexcept because
            // the expression within noexcept constructs and destroys a temporary
            << "Is T(rvalue T) noexcept? " << noexcept(T(std::declval<T>())) << '\n'
            << "Is T(lvalue T) noexcept? " << noexcept(T(t)) << '\n'
            << "Is U(rvalue U) noexcept? " << noexcept(U(std::declval<U>())) << '\n'
            << "Is U(lvalue U) noexcept? " << noexcept(U(u)) << '\n'
            << "Is V(rvalue V) noexcept? " << noexcept(V(std::declval<V>())) << '\n'
            << "Is V(lvalue V) noexcept? " << noexcept(V(v)) << '\n';
}
Is may_throw() noexcept? false
Is no_throw() noexcept? true
Is lmay_throw() noexcept? false
Is lno_throw() noexcept? true
Is ~T() noexcept? true
Is T(rvalue T) noexcept? true
Is T(lvalue T) noexcept? true
Is U(rvalue U) noexcept? false
Is U(lvalue U) noexcept? false
Is V(rvalue V) noexcept? true
Is V(lvalue V) noexcept? false

export(弃用):

参考文档C++ 关键词: export

nullptr:指代指针字面量。它是 std::nullptr_t 类型的纯右值。

参考文档nullptr,指针字面量

constexpr(在一些情况下能提高性能):constexpr 指定符声明可以在编译时求得函数或变量的值。

constexpr 变量必须满足下列要求:其类型必须是字面类型 (LiteralType) ,它必须被立即初始化,其初始化的完整表达式,包括所有隐式转换、构造函数调用等,都必须是常量表达式 。

参考博客constexpr:编译期与运行期之间的神秘关键字

int i;
const int size = i;
int arr[size];         //error,size不是常量表达式,不能在编译期确定

constexpr auto size = 10;
int arr[size];                     //OK,size时常量表达式

int i;
constexpr int size = i;  // error,i不能在编译期确定
constexpr int foo(int i) {
    return i + 5;
}

int main() {
    int i = 10;
    std::array<int, foo(5)> arr; // OK,5是常量表达式,计算出foo(5)也是常量表达式
    
    foo(i); // Call is Ok,i不是常量表达式,但仍然可以调用(constexpr 被忽略)
    
    std::array<int, foo(i)> arr1; // Error,但是foo(i)的调用结果不是常量表达式了
   
}

built-in类型是字面值常量,但是有时需要自定义类型也作为字面值常量,这时候就需需要将constexpr修饰构造函数。

class Point {
public:
    constexpr Point(double xval = 0, double yval = 0): x(xval), y(yval) { }
    constexpr double getX() const {return x;}
    constexpr double getY() const {return y;}
private:
    double x,y;
};

当这样定义一个类后,便可以将Point类型的对象定义为字面值常量。即:

constexpr Point p1(9.4, 27,7);
constexpr Point p2(28.8, 5.3);

constexpr
Point midpoint(const Point& p1, const Point& p2) {
    return {p1.getX() + p2.getX() / 2, p1.getY() + p2.getY() / 2} ;
}
 
constexpr auto mid = midpoint (p1, p2);

thread_local:能够在线程中创建一个全局变量或对象的本地副本,可以避免多线程情形下的资源竞争。

参考博客C++11 thread_local 关键词

线程存储期。对象的存储在线程开始时分配,而在线程结束时解分配。每个线程拥有其自身的对象实例。唯有声明为 thread_local 的对象拥有此存储期。 thread_local 能与 static 或 extern 一同出现,以调整链接。

#include <iostream>
#include <thread>
using namespace std;

thread_local int g_n = 1;

void f()
{
    g_n++;
    cout << "id=" << std::this_thread::get_id() << "  n=" << g_n << endl;
}

void foo()
{
    thread_local int i = 0;
    cout << "id=" << std::this_thread::get_id() << "  n=" << i << endl;
    i++;
}

void f2()
{
    foo();
    foo();
}

int main()
{
    g_n++;
    f();               // 3
    std::thread t1(f); // 2
    std::thread t2(f); // 2

    t1.join();
    t2.join();

    f2();               // 0 1
    std::thread t4(f2); // 0 1
    std::thread t5(f2); // 0 1

    t4.join();
    t5.join();
    return 0;
}

保证稳定性和兼容性

与C99兼容

1. C99预定义宏

功能
__STDC_HOSTED__编译器的目标系统环境中,包含标准C库,宏定义为1,否则为0。
__STDC__在C编译器中,表示编译器是否与C标准一致,C++编译器中由编译器决定这个宏是否定义,定义成什么值。
__STDC__VERSION__在C编译器中,表示编译器支持的标准版本,C++编译器中由编译器决定这个宏是否定义,定义成什么值。
__STDC_ISO_10646__用于表示C++编译环境符合某个版本的ISO/IEC 10646标准。
#include <iostream>
using namespace std;
int main()
{
    cout << "Standard Clib: " << __STDC_HOSTED__ << endl;
    cout << "Standard C: " << __STDC__ << endl;
    // cout << "C Standard Version: " << __STDC_VERSION__ << endl;
    cout << "ISO/IEC " << __STDC_ISO_10646__ << endl;
    return 0;
}
Standard Clib: 1
Standard C: 1
ISO/IEC 201706

2. __func__预定义标识符

返回所在函数的名字。

按标准定义,编译器会在函数定义之后,隐式定义__func__,所以将__func__作为函数参数默认值是不允许的。

#include <iostream>

using namespace std;

const char *hello() { return __func__; }
const char *world() { return __func__; }

int main(int argc, char const *argv[])
{
    cout << hello() << endl;
    cout << world() << endl;    
    return 0;
}
hello
world

3. _Pragma操作符

_Pragma (字符串字面值)

_Pragma("once") 等价于#pragma once,等价于:

#ifndef THIS_HEADER
#define THIS_HEADER
//
#endif

4. 变长参数的宏定义以及__VA_ARGS__

C99标准中,变长参数的宏定义是指在宏定义中,参数列表的最后一个参数为省略号,而预定义宏__VA_ARGS__则可以在宏定义的实现部分,替换省略号所代表的字符串。

#define PR(...) printf{__VA_ARGS__}
#include <cstdio>

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


int main(int argc, char const *argv[])
{
    int x = 3;
    LOG("x = %d", x);
    return 0;
}
tempCodeRunnerFile.cpp: Line 13:    x = 3

5. 宽窄字符串的连接

在之前char转换为wchar_t是UB,11标准中,char字符串和wchar_t字符串连接,会先将char转换为wchar_t,再连接字符串。

long long

正式加入C++标准,至少有64位长度。

#include <iostream>
#include <climits>
using namespace std;


int main(int argc, char const *argv[])
{
    cout << LLONG_MAX << endl;
    cout << LLONG_MIN << endl;
    cout << LLONG_WIDTH << endl;
    cout << ULLONG_MAX << endl;
    cout << ULLONG_WIDTH << endl;

    return 0;
}
9223372036854775807
-9223372036854775808
64
18446744073709551615
64

宏 __cpluscplus

常见的:

#ifdef __cplusplus
extern "C" {
#endif
    // some code here
# ifdef __cplusplus
}
#endif

但__cplusplus实际是一个随编译器的不同而变化的值:

03标准中,199711L。

11标准中,201103L。

我的编译器输出的是201402L。

所以我们可以:

# if __cplusplus < 201103L
    # error "should use C++11 implementation"
# endif

静态断言

noexcept

快速初始化成员变量

struct init {
    int a = 1; // 等号和大括号都可以就地对非静态成员变量初始化
    double b {1.2};
};

非静态成员的sizeof

在11标准中合法。

扩展的friend语法

class Poly;
typedef Poly P;

class LiLei{
    friend class Ploy; // OK

class HanMeimei{
    friend Ploy; // error: ‘Ploy’ does not name a type
};

class Jim{
    friend P; // OK 
};
class P;
template <typename T> class People{
    friend T;
};

People<P> PP; // P是PP的友元
People<int> Pi; // int 不是class,友元声明被忽略。

final/override

问题1:基类A中有虚函数f(),B继承A,重写了f(),C继承B:如何禁止C重写B的f()?

class B : public A{
    void f() final;
};

虚函数也是可以final的,但这使virtual关键字失去了意义。

问题2:基类A中有虚函数f(),B继承A,重写了f(),C继承B,重写了f():如何帮助程序员检查,他的确在C中重写了f()?

class C : public B{
    void f() override;
}

模板函数的默认模板参数

template <class T, class U = double>
void f(T t = 0, U u = 0);

void g()
{
    f(1, 'c');      // (1, 'c')
    f(1);           // (1,0) U使用了默认类型double
    f();            // 错误, T无法推导
    f<int>();       // (0, 0) U使用了默认类型double
    f<int, char>(); // (0, 0)
}

外部模板

a.c 中有int i,b.c 中想用这个int i, b.c中需声明:

extern int i;

则i这个符号的定义只在a.o中,b.o只记录了符号i会引用其他目标文件中的i。

这个语法可以用在模板上了。

局部和匿名类型作为模板实参

template <typename T>
class X
{
};

template <typename T>
void f(T t){};

struct A
{
} a;

struct
{
    int i;
} b; // 变量b的类型是匿名类型

typedef struct
{
    int i;
} B; // B是匿名类型

void g()
{
    struct C
    {
    } c; // C是局部类型,c 是局部变量

    X<A> x1; 

    X<B> x2;
    X<C> x3;

    f(a);

    f(b);
    f(c);
}

通用为本,专用为末

继承构造函数

原来:

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

class B : A
{
    B(int i) : A(i), d(i){};
    int d;
};

但,当基类拥有数量众多不同版本的构造函数时,太不方便了:

#include <string>

class A
{
  public:
    A(int i){};
    A(std::string str){};
};

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

int main(int argc, char const *argv[])
{
    B b("abc"); // error
    return 0;
}

有一个比较好的方式是使用using声明:

#include <string>

class A
{
  public:
    A(int i){};
    A(std::string str){};

};

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

int main(int argc, char const *argv[])
{
    B b1(123); // ok
    B b2("abc"); // ok
    return 0;
}

并且这种继承构造函数,如果没有被使用,是不会生成目标代码的,会更节省目标代码空间。

委派构造函数

class Info
{
  public:
    Info(){};
    Info(int i) : Info(){};
    Info(char *str) : Info(){};
}

不能同时委派构造函数和使用初始化列表。

右值引用:移动语义和完美转发

这里是个复杂难懂的部分。

移动语义

#include <iostream>
#include <utility>
using namespace std;

class Copy {
public:
  Copy() : d(new int(0)) { cout << "构造函数" << endl; };
  Copy(int i) : d(new int(i)) { cout << "构造函数 " <<  i << endl; };
  Copy(Copy &c) : d(new int(*c.d)) { cout << "拷贝构造函数" << endl; };
  Copy(Copy &&c) : d(c.d) {
    c.d = nullptr;
    cout << "移动构造函数" << endl;
  };
  int *d;
  ~Copy() {
    delete d;
    cout << "析构函数" << endl;
  };
};

Copy get_copy() { return Copy(); }

int main(int argc, char const *argv[]) {
  Copy c1 = get_copy();
  // 在这里应该调用移动构造函数,或者是拷贝构造函数,
  // 但实际被编译器优化成了和下面一行相同的语义。
  Copy c2;

  Copy c3(move(c2));//这里调用了移动构造函数
  return 0;
}

在C++中可以取地址的,有名字的为左值;没名字的,不能取地址的,称右值。

右值分两种:

  • 将亡值:将要被移动的对象,如返回右值引用T&&的函数的返回值,std::move()的返回值,或者转换为T&&的类型转换函数的返回值。
  • 纯右值:非引用返回的临时变量,以及不和对象关联的字面值,类型转换函数的返回值,lambda表达式。

std::move()能将左值强转为右值,继而可以通过右值引用来使用这个值。

完美转发

参考博客c++11 中的 move 与 forward

完美转发 (perfect forwarding),转发问题针对的是模板函数,这些函数主要处理的是这样一个问题:假设我们有这样一个模板函数,它的作用是:缓存一些 object,必要的时候创建新的。

template<class TYPE, class ARG>
TYPE* acquire_obj(ARG arg)
{
     static list<TYPE*> caches;
     TYPE* ret;

     if (!caches.empty())
     {
          ret = caches.pop_back();
          ret->reset(arg);
          return ret;
     }

     ret = new TYPE(arg);
     return ret;
}

这个模板函数的作用简单来说,就是转发一下参数 arg 给 TYPE 的 reset() 函数和构造函数,除此它就没再干别的事情,在这个函数当中,我们用了值传递的方式来传递参数,显然是比较低效的,多了次没必要的拷贝,于是我们准备改成传递引用的方式,同时考虑到要能接受 rvalue 作为参数,最后做出艰难的决定改成如下样子:

template<class TYPE, class ARG>
TYPE* acquire_obj(const ARG& arg)
{
    //...
}

但这样写很不灵活:

1) 首先,如果 reset() 或 TYPE 的构造函数不接受 const 类型的引用,那上述的函数就不能使用了,必须另外提供非 const TYPE& 的版本,参数一多的话,很麻烦。

2) 其次,如果 reset() 或 TYPE 的构造函数能够接受 rvalue 作为参数的话,这个特性在 acquire_obj() 里头也永远用不上。

其中1 好理解,2 是什么意思?

2 说的是这样的问题,即使 TYPE 存在 TYPE(TYPE&& other) 这样的构造函数,它在上述 acquire_obj() 中也永远不会被调用,原因是在 acquire_obj() 中,传递给 TYPE 构造函数的,永远是 lvalue(因为 arg 有名字),哪怕外面调用 acquire_obj() 时,用户传递进来的是 rvalue,请看如下示例:

holder get_holder();

holder* h = acquire_obj<holder, holder>(get_holder());

虽然在上面的代码中,我们传递给 acquire_obj() 的是一个 rvalue,但是在 acuire_obj() 内部,我们再使用这个参数时,它却永远是 lvalue,因为它有名字.

有名字的就是 lvalue。acquire_obj() 这个函数它的基本功能本来只是传发一下参数,理想状况下它不应该改变我们传递的参数的类型:假如我们传给它 lvalue,它就应该传 lvalue 给 TYPE,假如我们传 rvalue 给它,它就应该传 rvalue 给 TYPE,但上面的写法却没有做到这点,而在 c++11 以前也没法做到。forward() 函数的出现,就是为了解决这个问题。

forward() 函数的作用:它接受一个参数,然后返回该参数本来所对应的类型的引用。

理想的 acquire_obj() 原型,以及 forward() 原型。

template<class TYPE>
TYPE&& forward(typename remove_reference<TYPE>::type& arg)
{
   return static_cast<TYPE&&>(arg);
}

template<class TYPE, class ARG>
TYPE* acquire_obj(ARG&& arg)
{
   return new TYPE(forward<ARG>(arg));
}

显示转换操作符

参考博客杂货边角(15):C++11引入的explicit显式转换控制

C++此前编译器默认的隐式类型转换机制,在解决程序的泛型应用上起到了不小的帮助,但是这种隐式的类型转换有可能会导致潜在的问题。举代码如下:

#include <iostream>

using namespace std;

struct Rational_1 {
    Rational_1(int n=0, int d=1) : num(n), den(d) {
        cout << __func__ << "(" << num << "/" << den << ")" << endl;
    }
    int num;  //Numerator被除数
    int den;  //Denominator除数
};
struct Rational_2 {
    explicit Rational_2(int n=0, int d=1) : num(n), den(d) {
        //加了explicit关键字,代表该构造函数不能被隐式调用,必须显式调用才能运行
        cout << __func__ << "(" << num << "/" << den << ")" << endl;
    }
    int num;  //Numerator被除数
    int den;  //Denominator除数
};

void Display1(Rational_1 ra){
    cout << "Numerator: " << ra.num << " Denominator: " << ra.den << endl;
}

void Display2(Rational_2 ra){
    cout << "Numerator: " << ra.num << " Denominator: " << ra.den << endl;
}

template <typename T>
class Ptr {
public:
    Ptr(T* p) : _p(p) {}

    //定义一个从Ptr类型转换成bool类型的函数
    explicit operator bool() const {
        if (_p != 0)
            return true;
        else
            return false;
    }
private:
    T* _p;
};
/*************************************************************************
**1. explicit 关键字修饰函数,将会禁止函数被隐式调用
**  有些情况下,函数的隐式调用会导致意想不到的问题出现,
**  经常出现问题的场景有:拷贝构造函数的隐式传递调用 和 非显式的类型转换

*************************************************************************/
int main()
{
    Rational_1 r1_1 = 11;  //该表达式理应是调用拷贝赋值函数,但是因为r1_1并无初始化,所以将隐式调用带参构造函数
    Rational_1 r1_2(12);   //显式调用带参构造函数

    //Rational_2 r2_1 = 21;  //error: conversion from 'int' to non-scalar type 'Rational_2' requested
    Rational_2 r2_2(22);   //显式调用带参构造函数,可以编译通过

    Display1(1);  //参数传递,将隐式调用构造函数
    //Display2(2);  //无法通过编译,could not convert '2' from 'int' to 'Rational_2'
    Display2(Rational_2(2)); //显式调用构造函数传递临时对象

    int a;
    Ptr<int> p(&a);
    if (p)
        cout << "valid pointer." <<endl; //valid pointer
    else
        cout << "invalid pointer." << endl;

    Ptr<double> pd(0);
    cout << p+pd << endl; //如果不加explicit修饰Ptr类中的转换函数,则该表达式将会隐式调用
        //该转换函数,但是指针量之间的相加并无意义,但是因为转换函数的定义,该表达式将被编译通过
        //所以应该给转换函数加上explicit,以避免可能存在不当使用并且难以察觉
        //加上explicit修饰转换函数后,则除了在bool量应该出现的位置,是不会调用该转换函数的
        //所以编译器将报错:error: no match for 'operator+' (operand types are 'Ptr<int>' and 'Ptr<double>'
    return 0;
}

列表初始化

初始化列表

参考博客[
C++11新特性之列表初始化](https://blog.csdn.net/hailong...

在C++11中初始化列表被适用性被放大,可以作用于任何类型对象的初始化。如下:

class Foo
{
public:
  Foo(int) {}
private:
  Foo(const Foo &);
};
  
int _tmain(int argc, _TCHAR* argv[])
{
  Foo a1(123); //调用Foo(int)构造函数初始化
  Foo a2 = 123; //error Foo的拷贝构造函数声明为私有的,该处的初始化方式是隐式调用Foo(int)构造函数生成一个临时的匿名对象,再调用拷贝构造函数完成初始化
  
  Foo a3 = { 123 }; //列表初始化
  Foo a4 { 123 }; //列表初始化
  
  int a5 = { 3 };
  int a6 { 3 };
  return 0;
}

由上面的示例代码可以看出,在C++11中,列表初始化不仅能完成对普通类型的初始化,还能完成对类的列表初始化,需要注意的是a3 a4都是列表初始化,私有的拷贝并不影响它,仅调用类的构造函数而不需要拷贝构造函数,a4,a6的写法是C++98/03所不具备的,是C++11新增的写法。

同时列表初始化方法也适用于用new操作等圆括号进行初始化的地方,如下:

int* a = new int { 3 };
double b = double{ 12.12 };
int * arr = new int[] {1, 2, 3};

防止类型收窄

使用{},能够将可能数值越界的警告,转化为错误,从而禁止这种行为。

  const int x = 1024;
  const int y = 10;

  // 类型收窄 warning: overflow in implicit constant conversion
  char a = x;

  // 类型收窄 warning: overflow in implicit constant conversion
  char *b = new char(1024);

  // error: narrowing conversion of ‘1024’ from ‘int’ to ‘char’ inside { }
  // char c = {x};

  // ok
  char d = {y};

  // ok
  unsigned char e1 = -1;

  // error: narrowing conversion of ‘-1’ from ‘int’ to ‘unsigned char’
  // inside { }
  unsigned char e2 = {-1};

POD类型

Plain Old Data

Plain: 普通的
Old: 与C兼容:

  1. 可以用memset()初始化,memcopy()进行复制。
  2. 提供了对C内存兼容,C++程序可以与C函数进行相互操作。
  3. 保证了静态初始化的安全有效。

分为两类:

  • 平凡的:

    1. 有平凡的构造函数
    2. 有平凡的拷贝构造函数
    3. 有平凡的移动构造函数
    4. 有平凡的拷贝赋值运算符
    5. 有平凡的移动赋值运算符
    6. 有平凡的析构函数
    7. 不能包含虚函数
    8. 不能包含虚基类
#include <iostream>
#include <type_traits>

using namespace std;

class A {
  A() {}
};
class B {
  B(B &) {}
};
class C {
  C(C &&) {}
};
class D {
  D operator=(D &) {}
};
class E {
  E operator=(E &&) {}
};
class F {
  ~F() {}
};
class G {
  virtual void foo() = 0;
};
class H : G {};
class I {};

int main(int argc, char *argv[]) {
  cout << is_trivial<A>::value << endl; // 有不平凡的构造函数
  cout << is_trivial<B>::value << endl; // 有不平凡的拷贝构造函数
  cout << is_trivial<C>::value << endl; // 有不平凡的拷贝赋值运算符
  cout << is_trivial<D>::value << endl; // 有不平凡的拷贝赋值运算符
  cout << is_trivial<E>::value << endl; // 有不平凡的移动赋值运算符
  cout << is_trivial<F>::value << endl; // 有不平凡的析构函数
  cout << is_trivial<G>::value << endl; // 有虚函数
  cout << is_trivial<H>::value << endl; // 有虚基类

  cout << is_trivial<I>::value << endl; // 平凡的类
  return 0;
}
0
0
0
0
0
0
0
0
1
  • 标准布局的:

    1. 所有非静态成员有相同的访问权限
    2. 继承树中最多只能有一个类有非静态数据成员
    3. 子类的第一个非静态成员不可以是基类类型
    4. 没有虚函数
    5. 没有虚基类
    6. 所有非静态成员都符合标准布局类型
#include <iostream>
#include <type_traits>

using namespace std;

class A {
private:
  int a;

public:
  int b;
};

class B1 {
  static int x1;
};

class B2 {
  int x2;
};

class B : B1, B2 {
  int x;
};

class C1 {};
class C : C1 {
  C1 c;
};

class D {
  virtual void foo() = 0;
};
class E : D {};
class F {
  A x;
};

int main(int argc, char *argv[]) {
  cout << is_standard_layout<A>::value;
  cout << endl; // 违反定义1。成员a和b具有不同的访问权限
  cout << is_standard_layout<B>::value;
  cout << endl; // 违反定义2。继承树有两个(含)以上的类有非静态成员
  cout << is_standard_layout<C>::value;
  cout << endl; // 违反定义3。第一个非静态成员是基类类型
  cout << is_standard_layout<D>::value;
  cout << endl; // 违反定义4。有虚函数
  cout << is_standard_layout<E>::value;
  cout << endl; // 违反定义5。有虚基类
  cout << is_standard_layout<F>::value;
  cout << endl; // 违反定义6。非静态成员x不符合标准布局类型

  return 0;
}
0
0
0
0
0
0

非受限联合体

98标准中,非POD类型,不能成为union的数据成员。

11标准,任何非引用类型都可以,但不允许静态成员变量存在,union中的非POD数据成员的默认构造、析构、拷贝构造函数等也将被编译器删除。

#include <iostream>
#include <string>

using namespace std;

union T {
  string str;
  int n;
};

main(int argc, char const *argv[])
{
  T t; // 编译时报错。
  return 0;
}
#include <iostream>
#include <string>

using namespace std;

union T {
  string str;
  int n;
  public:
    T(){
      new (&str) string;
    }
    ~T(){
      str.~string();
    }
};

main(int argc, char const *argv[])
{
  T t; // ok
  return 0;
}
class Singer
{
public:
  Singer(int i) : id(i) {}
  Singer(string s) : str(s) {}
  ~Singer(){}; //需要显式声明
  union {
    string str;
    int id;
  };
};

int main(int argc, char const *argv[])
{
  Singer s1(1);
  Singer s2("abc");

  cout << s1.id << endl;
  cout << s2.str << endl;

  cout << s2.id << endl;
  cout << s1.str << endl;
  return 0;
}
1
abc
-1345386864
Segmentation fault (core dumped)

用户自定义字面量

#include <iostream>

long double operator"" _mm(long double x) { return x / 1000; }
long double operator"" _m(long double x)  { return x; }
long double operator"" _km(long double x) { return x * 1000; }

int main()
{
    std::cout << 1.0_mm << '\n';
    std::cout << 1.0_m  << '\n';
    std::cout << 1.0_km << '\n';
}
0.001
1
1000

这种新语法其实很容易理解:#include之后的三行代码定义了一个用户自定义的新的类型的操作符,称为字面量操作符 literal operator。在这个例子中,这个运算符能够转换相应的长度单位,例如,1 mm = 10-3 m,1 km = 103 m,而 1 m = 1 m。因此,我们的操作符就可以自动计算每个长度单位是多少米。

内联命名空间

namespace n1{
    namespace n2{
      int i;
    }

    inline namespace n3{
      int j;
    }

    #ifndef ABC
    inline
    #endif
    namespace n4{
      int k;
    }
    
    #ifndef DFG
    inline
    #endif
    namespace n5{
      int k;
    }
}

n1::n2::i;

n1::j;

模板的别名

template <typename T> 
using MapString = std::map<T, char*>;

MapString<int> numberedString;

一般化的SFINEA规则

Substitution failure is not an error.

匹配失败不是一个error。

对重载的模板的参数进行展开时,如果展开导致了一些类型不匹配,编译器不会报错。

例子:

#include <iostream>
using namespace std;
struct Test
{
  typedef int foo;
};

template <typename T>
void f(typename T::foo) { cout << "#1" << endl; }; // 第一个模板定义 - #1

template <typename T>
void f(T) { cout << "#2" << endl; }; // 第二个模板定义 - #2

int main(int argc, char const *argv[])
{
  f<Test>(10); // 调用 #1
  f<int>(10);  // 调用 #2 但因为SFINEA,尽管int::foo并不存在,也不会发生编译错误。
  return 0;
}
#1
#2

剩下书上说的我没看懂。

新手易学,老兵易用

右尖括号>的改进

Note: 此特性会造成一些与98标准的代码的不兼容。

// 98
typedef vector<int> vi;
vector<vi> vvi;     // ok
vector<vector<int>> // fail

// 11
vector<vector<int>> // ok

template <int i>
class X
{
};
X<1>> 5 > x;   // 98 ok; 11 fail
X<(1 << 5)> x; // ok

auto 类型推导

decltype

追踪返回类型

对返回类型进行推导。

template <typename T1, typename T2>
auto Sum(T1 &t1, T2 &t2) -> decltype(t1 + t2)
{
  return t1 + t2;
}

基于范围的for循环

vector<int> v = {1, 2, 3, 4, 5};
for (auto iter = v.begin(); iter != v.end(); iter++)
{
  cout << *iter << endl;
}

for (auto i : v)
{
  cout << i << end;
}

限制:迭代的对象的范围应该是可以确定的,比如标准库中各种拥有begin()end()的容器,或者是定长数组。

提高类型安全

强类型枚举

98标准枚举类型缺陷:

  1. 具有名字的enum类型的名字和enum成员的名字都是全局可见的。
enum A { X, Y, Z };

enum B { X, Y };
error: redeclaration of ‘X’
 enum B { X, Y };
          ^
note: previous declaration ‘A X’
 enum A { X, Y, Z };
          ^
error: redeclaration of ‘Y’
 enum B { X, Y };
             ^
note: previous declaration ‘A Y’
 enum A { X, Y, Z };
             ^
  1. 枚举的成员总是可以隐式转换成整型(有警告,但是不阻止这种行为)。
  2. 枚举类型所占空间大小由编译器来具体指定实现,不是一个确定量。

11标准的改善与扩展:

强类型枚举,也成为枚举类:

  • 强作用域
  • 不能和整形隐式互相转换
  • 默认底层存储类型为int,可以显示地指定底层类型(除了wchar_t以外的所有整型)。
enum class A{X, Y, Z};
enum class B :char{X, Y, Z};
//OK
A a = A::X;

堆内存管理:智能指针与垃圾回收

98标准中的auto_ptr被废弃了,以unique_ptrshared_ptrweak_ptr等智能指针替代。

#include <iostream>
#include <memory>

using namespace std;

int main(int argc, char const *argv[])
{
  unique_ptr<int> up1(new int(11)); // 不能被复制
  // unique_ptr<int> up2 = up1;        // error

  cout << "up1: " << *up1 << endl;

  unique_ptr<int> up3 = move(up1);

  if (up1 == nullptr)
    cout << "up1 is a nullptr" << endl;

  cout << "up3: " << *up3 << endl;

  up3.reset(); // 显式内存释放

  if (up3 == nullptr)
    cout << "u3 is a nullptr" << endl;

  shared_ptr<int> sp1(new int(22)); // 引用计数法实现GC
  shared_ptr<int> sp2 = sp1;        // ok,引用数加1
  weak_ptr<int> wp = sp1;           // ok, 引用计数不增加

  cout << "sp1: " << *sp1 << endl;
  cout << "sp2: " << *sp2 << endl;
  // cout << "wp2: " << *wp << endl; //  error: no match for ‘operator*’
  cout << "wp2: " << *wp.lock() << endl; 

  sp1.reset(); // 引用数减一,22的那片内存不会被释放。

  // cout << "sp1: " << *sp1 << endl;
  cout << "sp2: " << *sp2 << endl;
  cout << "wp2: " << *wp.lock() << endl; 

  sp2.reset();
  if(wp.lock() == nullptr)
    cout << "wp points to a nullptr" << endl;

  return 0;
}
up1: 11
up1 is a nullptr
up3: 11
u3 is a nullptr
sp1: 22
sp2: 22
wp2: 22
sp2: 22
wp2: 22
wp points to a nullptr

提高性能和操作硬件的能力

常量表达式

const 指定的是运行时常量。
constexpr能够指定编译时常量。

#include <iostream>
using namespace std;

const int get_const() { return 1; }
constexpr int get_constexpr() { return 2; }

int main(int argc, char const *argv[])
{
  int arr[get_const()] = {0}; 
  // 在老一些的版本的编译器应该是不能通过的 
  // variable "arr" may not be initialized
  int arr2[get_constexpr()] = {0};   
  return 0;
}

在函数返回类型前添加关键字constexpr可以使其成为常量表达式函数,但对这种函数有十分严格的要求:

  • 函数必须有返回值,且函数体只有单一的return返回语句。
  • 返回语句表达式必须是一个常量表达式。
  • 函数在使用前必须定义。
constexpr int f();
constexpr int a = f(); // error: ‘constexpr int f()’ used before its definition
const int i = 1;     // 如果在全局命名空间中,编译器一定会为i产生数据。
constexpr int j = 1; // 如果没有代码使用j,编译器可能不会为j产生数据。

应用:

#include <iostream>
using namespace std;

constexpr int fibonacci(int n)
{
  return (n == 1) ? 1 : ((n == 2) ? 1 : fibonacci(n - 1) + fibonacci(n - 2));
}

int main(int argc, char const *argv[])
{
  int fib[] = {fibonacci(10), fibonacci(20), fibonacci(30)}; 
  // 这个数组会在编译期计算出结果
  for (auto i : fib)
    cout << i << endl;
  return 0;
}

高级应用:与模板元编程对应的constexpr元编程。

变长模板

11标准库增加了一个新的类模板tuple,是pair扩展,是能够存放数量超过两个的数据集合。

std::tuple<double, char, std::string> collection;
// or
auto collection2 std::make_tuple(9.8, 'g', "gravity");

tuple模板能够接受变长的参数,也就是一种变长模板。

template <typename... Elements> class tuple; // 变长模板的声明

template <typename Head, typename... Tail>
class tuple<Head, Tail...> : private tuple<Tail...> {
  Head Head;
}; // 递归的偏特化定义

template <> class tuple<> {}; // 边界条件

一个tuple会展开成若干个子tuple

下面是一个函数的例子:

#include <iostream>
using namespace std;
void Printf(const char *s)
{
  while (*s)
  {
    if (*s == '%' && *++s != '%')
      return;
    cout << *s++;
  }
}

template <typename T, typename... Args>
void Printf(const char *s, T value, Args... args)
{
  while (*s)
  {
    if (*s == '%' && *++s != '%')
    {
      cout << value;
      return Printf(++s, args...);
    }
    cout << *s++;
  }
}

int main(int argc, char const *argv[])
{
  Printf("hello %s\n", string("world"));
  return 0;
}
hello world

原子类型和原子操作

<atomic>是一个很简单的库,大家自己看文档就好。 原子操作库

线程局部存储

quick_exit 和 at_quick_exit

有的时候,main或者使用exit函数调用结束程序的方式,不是那么令人满意:代码中有很多类,这些类在堆上分配了大量零散的内存,而mainexit调用会导致类的析构函数依次将这些零散的内存还给操作系统,这很费时。实际上,这些堆内存会在进程结束是由操作系统同以回收,这种方式的速度很快。如果这些堆内存对其他程序不产生影响的化,在程序结束时调用类的析构函数往往是无意义的。在这种情况下,我们也许需要更快地推出程序。

11标准引入了quick_exit函数来完成不调用析构函数的程序退出方式。与abort不同的是,quikc_exit不是异常退出,而是和exit一样的正常退出。同时我们可以像用atexit那样,使用at_quick_exit注册函数,在quick_exit时调用。

为改变思考方式而改变

指针空值nullptr

nullptr的类型是nullptr_t,能隐式转化成指针类型,用于替代使用在指针上的NULL

默认函数的控制。

默认函数:

  • 构造函数
  • 拷贝构造函数
  • 拷贝赋值函数
  • 移动构造函数
  • 移动拷贝函数
  • 析构函数

重用默认函数

#include <type_traits>
#include <iostream>

using namespace std;

class C1
{
public:
  C1() = default;
  C1(int i) : i(i) {}

private:
  int i;
};

class C2
{
public:
  C2() {}
  C2(int i) : i(i) {}

private:
  int i;
};

int main(int argc, char const *argv[])
{
  cout << "C1: " << is_pod<C1>::value << endl; // 1
  cout << "C2: " << is_pod<C2>::value << endl; // 0

  return 0;
}

删除默认函数

#include <iostream>
#include <type_traits>

using namespace std;

class C1
{
public:
  C1() = default;
  C1(const C1 &) = delete;
};

int main(int argc, char const *argv[])
{
  C1 c;
  C1 d(c); // error: use of deleted function ‘C1::C1(const C1&)’
  return 0;
}

一旦默认版本被删除了,该函数也不能被重载。

扩展用途:

  • 避免隐式类型转换。
  • 禁止在堆上分配该对象
#include <cstddef>

class C
{
public:
  C(int i) {}
  C(char c) = delete;
};
class C1
{
public:
  C1(int i) {}
};

class C2
{
public:
  void *operator new(std::size_t) = delete;
};

int main(int argc, char const *argv[])
{
  C1(1);   // ok
  C1('c'); // ok

  C(1);   // ok
  C('c'); // error: use of deleted function ‘C::C(char)’

  C2 *c = new C2; // error: use of deleted function ‘static void* C2::operator new(std::size_t)’
  return 0;
}

lambda函数

[capture](parameters) mutable->return-type { statement }

  • capture:捕捉列表,捕捉上下文中(父作用域)的变量,在lambda函数中使用,直接使用名称是值传递,加&是引用传递。按值传递的变量的值,在lambda函数创建的时候就确定了,引用传递的变量值则是在lambda函数使用时确定。
  • parameters:参数列表
  • mutable:lambda函数默认是const函数,mutable可以取消其常量性
  • return-type:返回类型,可以省略
  • statement:函数体

融入实际应用

对齐

alignofalignas

通用属性

C++有很多扩展语法,其中一项比较常见的就是“属性(attribute)”。属性是对语言中的实体对象(函数、变量、类型等)附加的一些额外注解信息,用来实现一些语言及非语言层面的功能,或是实现代码优化的一种手段。

g++中属性是通过__attribute__来声明的:

__attribute__ ((attribute-list))

具体有哪些属性,如何使用,需要查阅gcc的官方文档。

11标准统一使用了:

[[ attribute-list ]]

Unicode支持

原生字符串字面量

#include <iostream>
using namespace std;
int main(int argc, char const *argv[])
{
  cout<< R"(hello \n world)"<< endl; // hello \n world
  return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值