C++11中一些新语言特性与相似特性的比较

版权声明:本文为博主原创文章,只要标明出处即可转载。 https://blog.csdn.net/ariesjzj/article/details/52005044

原文地址:http://blog.csdn.net/jinzhuojun/article/details/52005044

众所周知,C++11中加入了不少语言新feature,其中一些用于替代之前C++标准中的相应feature。其中不少看起来都很像,或者表面看起来只是让代码更简洁。下面就列举了一些C++11 feature及之前标准中类似feature,看下它们有些什么更深的区别。

  • std::array vs. C-style array
std::array是C++11中引入的新容器,用于表示固定大小的数组,具有STL的接口。很多时候可以看作是封装了size(通过模板参数在编译时确定)的C-style array。由于它本质上就是C-style array的wrapper,因此具有与之相似的performance和memory footprint。和其它STL容器不同的是,它的元素内容都是存在其自身的,而不是从heap中分配。由于它的这些性质,与C-style array相比,主要优点有:
  1. 它不会自动转化为元素类型的指针。我们知道对于C-style array,type a[]与type *之间的转换是比较自由的,因为它们在汇编级别是同一个东西。只要把C-style array通过参数传递后,它的size信息就丢失了,所以我们经常要额外传一个整数来表示数组个数。而std::array不会有这样的问题。
  2. 初始化时,C-style array只能在声明时初始化。而std::array可以在之后用initializer list初始化元素。通过copy constructor和copy assignment,std::array还支持方便的拷贝操作。
  3. std::array支持STL标准库的通用接口。
  4. 用at()函数取元素时,如果越界会抛异常。可以避免stack上的越界访问,这类问题通常不借助工具比较难调。
缺点是在表达多维数组时略显罗嗦。

  • using(alias declarations) vs. typedef
C++11中用using关键字可以达到与typedef相似的定义alias的目的。与typedef相比,它的主要优点是可以被模板化,而typedef不能很方便地模板化。还有就是让一些alias看起来更简洁,不过这个就见仁见智了。

  • auto vs. explicit type declarations
auto是C++11中引入的类型推断机制。可以使代码免于声明变量类型。它在编译期完成,不会带来额外开销,因为anyway编译器需要得到类型信息。auto类似于template type deduction。唯一的不同是在braced initializer的处理上(如auto a{1},会创建一个只包含一个元素,即1的std::initializer_list<int>)。C++14中允许auto用于函数返回值。C++11中的decltype也是用了类似的type deduction,与auto的区别是它会严格返回类型,而auto会忽略掉reference,或const/volatile等修饰属性。decltype主要用于返回类型依赖于参数类型的情况,在C++14中可以用decltype(auto)进一步简化。

auto与显式类型声明相比,有下面优点:
  1. 可使代码简洁,少打字。
  2. 避免忘记初始化,且不用担心初始化的值与声明类型不匹配。
  3. 可以声明只有编译器知道的类型,如用lambda声明的closure(另一种方法是用std::function存储,但更费空间,且有OOM exception风险)。
    auto func = [](){
    	std::cout << "hello" << std::endl;
    };
    func();
缺点是代码可读性会有影响,虽然可以有一些trick在编译时或运行时得到实际的type。注意如果推断出的不是期望的类型,需要用显式类型初始化,即用static_cast强制转换一下再赋值给auto类型的变量。

  • uniform initialization with {} vs. creating objects with ()
C++中有各种对象初始化语法,如用(), =或{}。而uniform initialization通过braced initializer提供了一种通用的初始化方法。它的优点是:
  1. 统一的初始化语法。无论是一般变量,noncopyable对象还是类非静态成员,都可以用统一的语法来初始化。
  2. 用它还可以用多个值来初始化STL容器。如:
    std::vector<int> a{1, 2, 3, 4, 5};
    在C++11中还可以在类的声明中用{}对成员进行初始化。
    class A {
        int a{5};
    };
    这些用()都不行。
  3. 避免非法转换。如初始值无法用指定类型表示,在编译时即可暴露问题。
  4. 避免C++的most vexing parse,即语句A a();是声明一个没有参数并返回A的函数。这和直觉上会初始化a并调用A()构造函数是相悖的。
值得注意的是在构造函数重载时,braced initializer会优先选择std::initializer_list为参数的版本。另外用()和{}在某些时候会产生完全不同的结果(如std::list<int> v(5, 1)会创建5个元素1,而std::list<int> v{5, 1}会创建2个元素,一个5一个1)。

  • nullptr vs. 0(or NULL)
在C/C++中,0及很多情况下NULL都不是指针类型,而是整型。 nullptr用来替代0或NULL,实际类型为std::nullptr_t ,用来表示所有类型的指针。它有以下优点:
  1. 在重载时能正确找到参数为指针的版本。比如foo(NULL)未必会调用参数为指针的版本,而foo(nullptr)则可以。nullptr代表任意类型指针,可以让编译器正确解析成参数为指针的版本。
  2. 检查指针是否合法时,用nullptr可以使代码更简洁易懂。
  3. 使用模板时,涉及到type deduction时,如果用0或NULL会被判定为整型,如果所需参数为指针类型,会编译出错。

  • enum class vs. enums
enum class,又称为scoped enum。它为枚举类型增加了scope。相对地,传统enum就成为unscoped enum。它的优点主要包括:
  1. 避免了名字泄露导致的命名污染。因为传统enum是在当前scope都可见的。这点上,和定义一个class再在里边定义enum起到一样的效果。
  2. enum class不能和其它类型隐式转换,因此避免了不当的类型转换。
  3. 因为它有默认的确定的underlying type,因此可以方便地支持forward declaration。如果要指定underlying type,可以用如下方式:
    enum class A : char {
    	a, b
    };
  • delete vs. private undefined special member functions
delete是和default一起在C++11中被引入,作为函数的修饰符。如果想让用户不调用特殊成员函数(如拷贝构造),传统方法是将它们置为private且不要定义。这样类外部无法调用,内部也会因为链接出错无法调用。那么,相比之下,delete的优点在于:
  1. 被定义成delete的成员函数,即使是其它成员函数或friend函数也无法使用,并且会提前在编译时就报错,而不需要等到链接时。
  2. 不仅可用于成员函数,还可以用于其它场合,比如禁用某一类型的重载函数,或是用于禁用模板实例化时的某一些特化(template specialization)。
注意delete函数一般放在public,这样可以使出错信息不混淆,否则得到的信息可能是因为private所以无法访问。

  • constexpr vs. const
constexpr既可以用于对象,也可以用于函数。用于对象时,表示该对象在编译(或链接)时即可确定。因此,它可用于优化(将运行时的计算提前到编译时),也可以用于一些需要整型常量表达式的情况(如std::array长度,枚举值等)。而相比较之下,const并不保证在编译期能知道其值,只是强调其值一旦确定不能更改。因此用constexpr常量如果编译时无法确定值会编译出错:
int a;
constexpr int ca = a;
而const不会:
int a;
const int ca = a;
当constexpr用于函数时,它表示如果调用它们时传入编译期常量,则它们也会返回编译期常量,这意味着计算在编译期完成。但如果不是,那它就是个普通函数。注意和C++11相比,C++14中constexpr函数中语句和返回类型的限制更加宽松。

  • noexcpt vs. throw
C++11中,noexcept用于标识那些不会抛异常的函数。比如默认所有内存分配函数及析构函数都是noexcept的。这相当于是一种函数间的contract,因此如果在声明不会抛异常的函数中抛了异常,会导致程序直接退出。声明为noexcept会有助于编译器做优化,比如一些有强异常安全假设的STL容器会优先使用move操作,但只有其move操作是noexcept的前提下(通过std::move_if_noexcept)。在之前的C++标准中,类似的功能用的是throw()。它们的主要区别在于:
  1. 语法上,throw的参数为exception类型,表明会抛何种异常;而noexcept为bool,表明会或不会抛异常。另外noexcept的参数可以是一个编译期确定的复杂表达式(比如通过type trait中的std::is_nothrow_default_constructible等)。这就允许在模板类中根据模板参数来确定是否会抛异常。
  2. 与throw()相比,noexcept能让编译器更好地优化,因为noexcept在一些情况下可以避免维护回溯信息。

  • emplace/emplace_back vs. insert/push_back
针对STL容器,emplace/emplace_front/emplace_back/emplace_after分别用于原insert/push_front/push_back/insert_after等函数的场景。emplace系函数利用move semantics减少临时对象的构造,从而减少了开销。以vector的push_back和emplace_back为例,如果插入的元素不是vector中的元素类型。在参数都是临时对象(即rvalue)的前提下。如果是push_back,则需要调用一次constructor,一次move constructor和一次析构。而emplace_back只需要一次构造。
std::vector<std::string> v;
v.reserve(2);
v.push_back("aaa");
v.emplace_back("bbb");
可以看到,emplace_back版本主要省了那两次临时对象的构造和析构。在性能方面,理论上emplace系函数性能应不低于其替代函数。

emplace_back采用了perfect forwarding技术,它的参数就是元素的构造函数的参数。这与push_back是不同的。这点差异会引起一些容易出错的地方,比如如果需要emplace_back的单个参数来构造元素,则不算作隐式转化,因此explicit关键字不起作用。

  • rvalue reference(&&) vs. lvalue reference(&)
我们知道申明lvalue reference用type&,C++11中若要申明rvalue reference可用T&&。但T&&不仅仅表示rvalue reference,它还可能是universal reference/forwarding reference(需要type deduction时,如作为template parameter或auto类型时),即既可能为rvalue reference,又可能为lvalue reference。通过rvalue reference,C++11中的参数可区分lvalue和rvalue。比如:
void setValue(CustomType&& v);
void setValue(CustomType& v);
这样,当参数为lvalue或rvalue时会通过重载调用相应的版本。

它的应用很广泛。比如STL中的push_back在C++11实现中就加入了参数为rvalue reference的版本:
void push_back(T&& x);
其它还有很多应用,比较典型比如C++11中加入的special member function:move constructor和move assignment operator。
A (A &&);
A& A:: operator=(A && );
与之相关的比较重要的两个函数是std::move和std::forward。两者本质都是将参数cast为rvalue reference,区别是前者是无条件的,后者是有条件的(仅在参数本身为rvalue时)。

猜你喜欢

转载自blog.csdn.net/ariesjzj/article/details/52005044