C++11之后的一些新特性

RAII

RAII(Resource Acquisition Is Initialization, 资源获取即初始化),指资源在拿到时就已经初始化,不需要的时候可以自动释放该资源。可以节省代码手动释放资源的复杂度。

在C++中的表现形式一般为将资源封装为类,然后在构造函数中获得资源(变量对象、锁),在析构函数中释放资源。

class SomeLockGuard {
public:
    SomeLockGuard() {
        //加锁
        m_lock.lock();
    }
    ~SomeLockGuard() {
        //解锁
        m_lock.unlock();
    }
private:
    SomeLock  m_lock;
};

void SomeFunction() {
    SomeLockGuard lockWrapper;
    if (条件1) {
        if (条件2) {
            某些操作1
            return;
        }
        else (条件3) {
            某些操作2
            return;
        }
    }

    if (条件3) {
        某些操作3
        return;
    }

    某些操作4
}

pimpl惯用法

在设计库时,为了尽量减少对外暴露的类成员变量和私有成员函数,可以把这些需要隐藏的内容放到一个类私有的类指针变量中,而其具体实现不在头文件内,达到对用户透明的效果。

//.h文件
class CSocketClient {
public:
    CSocketClient();
    ~CSocketClient();
//省略重复的代码
private:
    class Impl;
    Impl* m_pImpl;
};
//.cpp文件
class CSocketClient::Impl {
public:
    void LoadConfig() {...}

public:
    //变量定义
    ...
};

CSocketClient::CSocketClient() {
    m_pImpl = new Impl();
}
CSocketClient::~CSocketClient() {
    delete m_pImpl;
}

统一的类成员初始化语法与std::initializer_list

C++11之前的类成员变量,要么在构造函数内一个个初始化,要么声明为static const为其赋初值。
C++11开始支持类成员变量的大括号初始化语法,与局部变量的初始化语法统一起来了。

class A {
    public:
    A() : arr{2, 0, 1, 9} {}
public:
    int arr[4];
    bool ma{true};
    std::string mc{"hello world"};
};

通过使用std::initializer_list<T>模板对象,可以让自定义类也支持这种大括号的初始化语法。

#include <initializer_list>
#include <iostream>
#include <vector>

class A {
 public:
  A(std::initializer_list<int> integers) {
    m_vecIntegers.insert(m_vecIntegers.end(), integers.begin(), integers.end());
  }

  ~A() {}

  void append(std::initializer_list<int> integers) {
    m_vecIntegers.insert(m_vecIntegers.end(), integers.begin(), integers.end());
  }

  void print() {
    size_t size = m_vecIntegers.size();
    for (size_t i = 0; i < size; ++i) {
      std::cout << m_vecIntegers[i] << std::endl;
    }
  }

 private:
  std::vector<int> m_vecIntegers;
};

int main() {
  A a{1, 2, 3};
  a.print();

  std::cout << "After appending..." << std::endl;

  a.append({4, 5, 6});
  a.print();

  return 0;
}

对于稍微复杂的对象,如嵌套的JSON数据,可以先定义基本NODE的多种构造函数,然后再封装一层initializer_list<NODE>的类。

initializer_list<T>除了提供了构造函数,还提供了三个成员函数:

//返回列表中的元素个数
size_type size() const;
//返回第一个元素的指针
const T* begin() const;
//返回最后一个元素的下一个位置,代表结束
const T* end() const;

C++17注解标签(attributes)

使用注解标签的语法:[[attributes]] types/functions/enums/etc

这些标签可以用于修饰任意类型、函数、enumeration,C++17前不能修饰namespace和enumerator,C++17取消了该限制。

C++98/03的enumeration和C++11的enumerator

enumeration:不限定作用域的枚举。定义的枚举所在的作用域,无法再定义与枚举变量同名的变量。

enum color {
    black,
    white,
    red
};

//无法编译通过
bool white = true;

enumerator:限定作用域的枚举。枚举值对外部不可见,必须通过color::white引用,所以可以定义同名的变量。

enum color {
    black,
    white,
    red
};

//可以编译通过
bool white = true;

C++17的注解标签

1、[[noreturn]]

告诉编译器某个函数没有返回值,
例如:[[noreturn]] void terminate();

此标签一般在设计一些系统函数时使用,例如std::abort()std::exit()

2、 [[deprecated]]

表示一个函数或者类型已经被弃用,使用被弃用的函数或者类型时,在编译时,部分编译器警告,部分直接报错。

[[deprecated]] void funX();

[[deprecated("use funY instead")]] void funX();

3、 [[fallthrough]]

用于switch-case分支语句中,没有break语句时,提示编译器这是有意为之的,不要报警告。

4、 [[nodiscard]]

[[nodiscard]]用于修饰函数,告诉函数调用者必须关注函数的返回值。

5、 [[maybe_unused]]

通常情况下,编译器会给未使用的函数或者变量给出警告,还有一些编译器直接不允许通过编译。通常的做法是写一句无关调用语句:

int x;
x; //used

而使用了[[maybe_unused]]就不必这样。

final

final关键字修饰一个类,这个类将不允许被继承。

class A final {};
class B : A {}; //error

override

类方法被override关键字修饰,表明该方法重写了父类的同名方法。这可以解决virtual关键字带来的两个问题:

①无论子类重写的方法是否添加virtual关键字,都无法直观确定该方法是否是重写的父类的方法。

②子类在重写时,由于函数名错误或者参数错误,导致方法变成独立的子类新加方法,且编译器检查不到。

使用override关键字修饰后,编译器会强制检查,如果父类不存在该方法,报错。

=default语法

该语法让编译器自己生成默认构造、析构函数。

=delete语法

可以禁止编译器生成该函数,达到禁止某些操作的目的。

一般在一些工具类中,不需要用到构造函数、析构函数、拷贝构造函数、operator=这4个函数,于是可以使用=delete,防止编译器生成,同时减少生成的可执行文件的体积。

auto关键字的用法

auto一般用于让编译器自动推导一些复杂的模板数据类型,以简化语法。

自定义对象如何支持range-based循环语法

为了支持这种语法,自定义对象至少要实现如下两种方法:

Iterator begin();
Iterator end();

上面的Iterator是自定义数据类型的迭代子类型,这里的Iterator类型必须支持如下操作:

① operator++(自增)操作,可以在自增之后返回下一个迭代子的位置

② operator!=(不等于)操作

③ operator*(解引用,dereference)操作

#include <iostream>
#include <string>

template<class T, size_t N>
class A {
public:
    A() {
        for(size_t i = 0; i < N; i++) m_ele[i] = i;
    }
    ~A() {}
    T* begin() { return m_ele + 0;}
    T* end() {return m_ele + N;}
private:
    T m_ele[N];
};

int main() {
    A<int, 10> a;
    for(auto iter : a) std::cout << iter << std::endl;
    return 0;
}

上述例子中,迭代子Iterator是T*,是指针类型,本身支持自增、不等于、解引用等操作,故不需要自己实现。

for-each循环的实现原理

for-each循环可抽象成如下公式:

for(for-range-declaration: for-range-initializer)
    statement;

C++17标准中要求编译器解释for-each循环为如下形式:

auto && __range = for-range-initializer;
auto __begin = begin-expr;
auto __end = end-expr;
for(; __begin != __end; ++ __begin) {
    for-range=declaration = *__begin;
        statement;
}

由此可以知道自定义类需要重载的方法以及起始迭代子和尾部迭代子可以为不同类型。

C++17结构化绑定

std::tuple的用法类似高维的std::pair:

std::tuple<std::string, std::string, int, int, std::string> info("a", "b", 0, 0, "res");

对于tuple对象某维元素的获取,可以使用std::get<N>,但是这还不够方便。在C++17中引入了结构化绑定,语法如下:

auto [a, b, c, ...] = expression;
auto [a, b, c, ...] = {expression};
auto [a, b, c, ...] = (expression);

右边的expression可以是一个函数调用、大括号表达式或者支持结构化绑定的某个类型变量。

auto [iterator, inserted] = someMap.insert(...);

double myArray[3] = {...};
auto [a, b, c] = myArray;

struct Point {
    double x;
    double y;
};
Point myPoint(10.0, 20.0);
auto [mx, my] = myPoint;
auto& [mx_, my_] = myPoint;
const auto& [sx, sy] = myPoint;

结构化绑定可以配合for-each的循环语法使用。

std::unordered_map<std::string, int> strMap;
...
for(const auto& [str, sz] : strMap) {
    ...
}
posted @ 2022-11-15 19:47  仙人修凡  阅读(41)  评论(0)    收藏  举报