C++新标准归纳

/ 0评 / 0

本文将归纳C++11及以后的的常用新标准。不常用的可能不会被写上去。

智能指针 (C++11)

参考:https://zh.cppreference.com/w/cpp/header/memory

利用RAII的原理,自动管理堆内存的生命周期。

  1. uniqu_ptr:只允许移动构造的智能指针(即只允许存在一个实例)。只能按引用传递。在离开作用域后立刻释放指针。
  2. shared_ptr:可以复制的智能指针,复制时引用计数+1,析构时-1。当引用计数为0时释放指针。
  3. weak_ptr:不会增加shared_ptr引用计数智能指针,用于避免循环引用的情况(参考这里)。调用指针所指对象时,会临时提升为shared_ptr,用完之后回到weak_ptr。
  4. auto_ptr:反直觉的智能指针,在C++17被移除。

引用计数的基础是shared_ptr知道其他shared_ptr对象的存在。如果直接用裸指针赋值,则shared_ptr不会正确配置引用计数,还可能导致重复释放。

例如有两个shared_ptr A和B,它们用reset方法同时指向一个对象O。则A和B的引用计数都是1。当A释放时,O会被释放一次,B释放时,O又被释放一次。第二次释放时将导致运行时错误。正确的方法是A指向O后,B调用A的复制构造产生。即形如shared_ptr B = A而不是shared_ptr B(O)。这样B在构造时知道A的存在,就可以与A共享同一个引用计数器(而不是重新设置一个)。

避免裸指针和智能指针同时使用。容易出错。

auto (C++11)

这也是一个被复用的关键字。在C++11之前表示自动管理变量生命周期,由于压根没什么人用因此在C++11之后被改成了自动推导类型。这个关键字我自己也用的比较多,可以说没了auto已经没办法好好写程序了。

auto是编译期的,因此没有运行时开销。通过变量的赋值表达式推导变量的类型,形如auto a = 1+1.0,其中a就会被推导为double(1被推导为int,1.0默认是double而不是float)。

除了声明变量,还可以用在for循环中:

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

编译器将按需调用begin() / end()方法和begin(T) / end(T)尝试构造范围循环。如果是数组,则按照数组下标进行。

在C++11中,允许使用auto作为函数返回类型的占位符,并使用一个拖尾声明指定具体类型。这种格式主要允许使用decltype从函参中推导返回值。例如 auto foo(int a, double b) -> decltype(a+b)。并不能让编译器自动从return语句推导类型。

在C++14中,允许使用decltype(auto)auto指定函数返回值。编译器通过return语句推导具体类型,不需要手动指定。

在C++20中,还可以用auto标记函参类型。编译期根据调用时传入的参数推导具体的函数。比较像模板。

类型推导还有decltype,这个关键字可以从表达式中推导一个类型。但只能对具体的对象进行运算。是编译期过程。

lambda表达式

使用形如[](Param para){}的表达式表示一个lambda表达式对象。其中中括号表示捕获的变量,圆括号放函参,花括号是函数体,被指定为表达式对象()的运算符重载。可以用拖尾指定返回类型,也可以让编译器自己推导。如果要设定一个lambda表达式的左值,由于类型比较复杂,最好用auto让编译器推导,例如:

auto f = [](int a, int b){return a < b;};

lambda表达式的类型是唯一的,同一个表达式在不同位置也可能产生不同的类型。因此不要向lambda表达式的变量赋值:

auto lambda = [](){reutrn true;};
lambda = [](){return true;}; // error

由于捕获的性质,可以将调用者以外的内容传入,例如:

template<typename Func> void foo(Func fun) {
    cout << fun();
}

int main()
{
    int a = 1;
    foo( [a]() {return a;} ); // output: 1
    //    ^ 使用方括号捕获变量a并带进foo中
    return 0;
}

移动语义和完美转发 (C++11)

(详细内容建议参考Effective Mordern C++)

C++的值分为两类,左值和右值。左值可以取地址,右值则不行。右值的作用域仅限于当前表达式。表达式结束则立刻被销毁。

右值引用允许进行移动构造。由于右值离开表达式后被析构,因此可以直接将右值对象的资源转移到新对象的名下,从而砍掉了大量的复制开销。STL容器全线支持移动语义,因此返回std::map、std::set、std::vector之类的对象时,不会把整个容器都复制一遍。

std::move用于实现移动语义,无条件地将左值引用转换为右值引用。

std::forward用于完美转发。所有的函参都是左值,因此想要让函参被推导为右值引用,使用std::forward。但std::forward会进行判断,如果这个函参确实是一个右值,则std::forward会将其转换为右值引用,否则还是保持左值推导。

移动语义不移动,完美转发既不完美也不转发。这两个其实是完全编译期的,并且不会产生任何运行时代码。总之“移动语义”和“完美转发”是非常suck的翻译,还是按照英文名理解比较好。

右值引用的符号是&&,但是如果涉及类型推导,则可能成为万能引用。万能引用的成立条件较为苛刻,添加修饰词就有可能导致万能引用被破坏,例如const T&&还是会被推导为T的右值引用。

要成为万能引用,被推导的参数必须在调用函数时确认,而不能在此之前确认。例如std::vector的方法push_back使用了T&&,但是这个T早在std::vector对象声明之时就被指定,因此具体调用push_back时并不涉及类型推导。

另外&&必须修饰在被推导的类型上。例如std::vector<T>&&依然是一个确定的右值引用。因为&&修饰的是std::vector而不是T。

万能引用修饰的参数可能被推导成任何形式,包括但不限于左值、右值、左值引用、右值引用、const修饰、volatile修饰等。这也是std::forward的用武之地。

Effective Mordern C++建议针对最后一次右值引用使用std::move,对最后一次万能引用使用std::forward。并且不要乱加const,因为const可能让右值引用的版本无法运作(例如调用非const方法)而推导失败、退化到copy only的版本。

如果可能有返回值优化,不要使用std::move和std::forward。

如果有一个万能引用的版本,不要重载。例如函数foo(T&&)是一个接受万能引用的函数,同时特化一个foo(int)重载。如果此时传入一个short类型,那么实际上有两种选择,一种是把short提升到int,匹配int版本;另一种是匹配T&&版本实例化出一个short版本。如果T&&版本没法接收数字类型,就会挂掉。

实际上除非函数签名能完整匹配上,否则编译器优先从模板里实例化出一个。又因为万能引用几乎能匹配所有类型,所以基本上除了特化的版本以外,其他调用都会往T&&上靠。

SFINAE

全称Substitution failure is not an error。即模板推导失败不是错误。

经典应用为std::enable_if。该模板接收一个bool量作为模板参数。进一步地将enable_if<Expression>::type作为一个模板参数,如果Expression为假,则模板参数就会推导失败,也就阻止了编译器匹配这个模板,而是转而寻找其他匹配。该机制依托于SFINAE,否则一旦推导失败就报错,是没办法进行下去的。

属性列表 (C++11)

属性列表用于修饰函数或变量。冠在声明之前,用两层方括号包裹,形如:

[[noreturn]] [[nodiscard]] int foo()

属性列表是编译期的,用于指示编译器对一些情况做出响应。例如noreturn表示这个函数不应该返回,nodiscard表示函数返回值不应被抛弃。如果发生了相应的状况,则编译器会抛出警告。

C++标准规定了一些值,编译器自己可能也规定了一些。具体的要查表才知道。

std::functional

一种函数包装器,可以将所有的callable包装为对象。包括函数、lambda表达式、函数指针、仿函数等等。还可以进行高级的绑定操作,提供更高的灵活性。

杂项

  1. default:当提供了构造函数时,编译器不再提供默认构造函数。此时用ClassName() = default可以将之声明为默认构造函数。
  2. delete:禁用成员函数,用法同上。如果调用了被delete的方法则编译器报错。std::unique_ptr就是这样禁止拷贝构造的。
  3. override:用于成员函数,形如void foobar() override;,显式说明该方法重写(不是重载)了基类的方法。如果没有发生重写,则编译器报错。
  4. final:用于成员函数。禁止函数被重写。用法同上。
  5. static_assert:静态断言。形如static_assert(Expression, ErrMsg)。断言失败时提示编译错误,并抛出ErrMsg作为错误信息。
  6. nullptr:C++11引入的关键字,是明确的指针类型,表示空指针。在此之前NULL和0有可能冲突。

发表评论

邮箱地址不会被公开。 必填项已用*标注