zoukankan      html  css  js  c++  java
  • c++ 从vector扩容看noexcept应用场景

    c++11提供了关键字noexcept,用来指明某个函数无法——或不打算——抛出异常:

    void foo() noexcept;             // a function specified as will never throw
    void foo2() noexcept(true);      // same as foo
    void bar();                      // a function might throw exception
    void bar2() noexcept(false);     // same as bar
    

    所以我们需要了解以下两点:

    • noexcept有什么优点,例如性能、可读性等等。
    • 需不需要在代码中大量使用noexcept

    noexcept优点

    我们先从std::vector入手来看一下第一点。

    我们知道,vector有自己的capacity,当我们调用push_back但是vector容量满时,vector会申请一片更大的空间给新容器,将容器内原有的元素copy到新容器内:

    image

    但是如果在扩容元素时出现异常怎么办?

    • 申请新空间时出现异常:旧vector还是保持原有状态,抛出的异常交由用户自己处理。
    • copy元素时出现异常:所有已经被copy的元素利用元素的析构函数释放,已经分配的空间释放掉,抛出的异常交由用户自己处理。

    这种扩容方式比较完美,有异常时也会保持上游调用push_back时原有的状态。

    但是为什么说比较完美,因为这里扩容还是copy的,当vector内是一个类且持有资源较多时,这会很耗时。所以c++11推出了一个新特性:move,它会将资源从旧元素中“偷”给新元素(对move不熟悉的同学可以自己查下资料,这里不展开说了)。应用到vector扩容的场景中:当vector中的元素的移动拷贝构造函数是noexcept时,vector就不会使用copy方式,而是使用move方式将旧容器的元素放到新容器中:

    image

    利用move的交换类资源所有权的特性,使用vector扩容效率大大提高,但是当发生异常时怎么办:
    原有容器的状态已经被破坏,有部分元素的资源已经被偷走。若要恢复会极大增加代码的复杂性和不可预测性。所以只有当vector中元素的move constructornoexcept时,vector扩容才会采取move方式来提高性能。

    刚才总结了利用noexcept如何提高vector扩容。实际上,noexcept还大量应用在swap函数和move assignment中,原理都是一样的。

    noexcept使用场景

    上面提到了noexcept可以使用的场景:

    • move constructor
    • move assignment
    • swap

    很多人的第一念头可能是:我的函数现在看起来明显不会抛异常,又说声明noexcept编译器可以生成更高效的代码,那能加就加呗。但是事实是这样吗?

    这个问题想要讨论清楚,我们首先需要知道以下几点:

    • 函数自己不抛异常,但是不代表它们内部的调用不会抛出异常,并且编译器不会提供调用者与被调用者的noexcept一致性检查,例如下述代码是合法的:
    void g(){
        ...       //some code
    }
    void f() noexcept
    {
        … 			//some code
        g();
    }
    
    • 当一个声明为noexcept的函数抛出异常时,程序会被终止并调用std::terminate();

    所以在我们的代码内部调用复杂,链路较长,且随时有可能加入新feature时,过早给函数加上noexcept可能不是一个好的选择,因为noexcept一旦加上,后续再去掉也会变得困难 : 调用方有可能看到你的函数声明为noexcept,调用方也会声明为noexcept。但是当你把函数的noexcept去掉却没有修改调用方的代码时,当异常抛出到调用方会导致程序终止。

    目前主流的观点是:

    • 加noexcept
      • 函数在c++98版本中已经被声明为throw()
      • 上文提到过的三种情况:move constructor、move assignmemt、swap。如果这些实现不抛出异常,一定要使用noexcept
      • leaf function. 例如获取类成员变量,类成员变量的简单运算等。下面是stl的正向iterator中的几个成员函数:
    # if __cplusplus >= 201103L
    #  define _GLIBCXX_NOEXCEPT noexcept
    # else
    #  define _GLIBCXX_NOEXCEPT
    
     reference
          operator*() const _GLIBCXX_NOEXCEPT
          { return *_M_current; }
    
          pointer
          operator->() const _GLIBCXX_NOEXCEPT
          { return _M_current; }
    
          __normal_iterator&
          operator++() _GLIBCXX_NOEXCEPT
          {
    	++_M_current;
    	return *this;
          }
    
          __normal_iterator
          operator++(int) _GLIBCXX_NOEXCEPT
          { return __normal_iterator(_M_current++); }
    
    • 不加noexcept
      除了上面的要加的情况,其余的函数不要加noexcept就可以。

    最后我们看一下vector如何实现利用noexcept move constructor扩容以及move constructor是否声明noexcept对扩容的性能影响。

    如何实现利用noexcept move constructor扩容

    这里就不贴大段的代码了,每个平台的实现可能都不一样,我们只关注vector是怎么判断调用copy constructor还是move constructor的。

    其中利用到的核心技术有:

    • type trait
    • iterator trait
    • move iterator
    • std::forward

    核心代码:

    template <typename _Iterator, typename _ReturnType = typename conditional<
                                      __move_if_noexcept_cond<typename iterator_traits<_Iterator>::value_type>::value,
                                      _Iterator, move_iterator<_Iterator>>::type>
    inline _GLIBCXX17_CONSTEXPR _ReturnType __make_move_if_noexcept_iterator(_Iterator __i) {
      return _ReturnType(__i);
    }
    
    template <typename _Tp>
    struct __move_if_noexcept_cond
        : public __and_<__not_<is_nothrow_move_constructible<_Tp>>, is_copy_constructible<_Tp>>::type {};
    

    这里用type traititerator trait联合判断:假如元素有noexcept move constructor,那么is_nothrow_move_constructible=1 => __move_if_noexcept_cond=0 => __make_move_if_noexcept_iterator返回一个move iterator。这里move iterator迭代器适配器也是一个c++11新特性,用来将任何对底层元素的处理转换为一个move操作,例如:

    std::list<std::string> s;
    std::vector<string> v(make_move_iterator(s.begin()),make_move_iterator(s.end()));     //make_move_iterator返回一个std::move_iterator
    

    然后上游利用生成的move iterator进行循环元素move:

    {
      for (; __first != __last; ++__first, (void)++__cur) std::_Construct(std::__addressof(*__cur), *__first);
      return __cur;
    }
    
    template <typename _T1, typename... _Args>
    inline void _Construct(_T1 *__p, _Args &&... __args) {
      ::new (static_cast<void *>(__p)) _T1(std::forward<_Args>(__args)...);      //实际copy(或者move)元素
    }
    

    其中_Construct就是实际copy(或者move)元素的函数。这里很关键的一点是:对move iterator进行解引用操作,返回的是一个右值引用。,这也就保证了,当__first类型是move iterator时,用_T1(std::forward<_Args>(__args)...进行“完美转发”才调用_T1类型的move constructor,生成的新对象被放到新vector的__p地址中。

    总结一下过程就是:

    • 利用type traititerator trait生成指向旧容器的normal iterator或者move iterator
    • 循环将旧容器的元素搬到新容器。如果指向旧容器的是move iterator,那么解引用会返回右值引用,会调用元素的move constructor,否则调用copy constructor

    大家可以用下面这段简单的代码在自己的平台打断点调试一下:

    class A {
     public:
      A() { std::cout << "constructor" << std::endl; }
      A(const A &a) { std::cout << "copy constructor" << std::endl; }
      A(const A &&a) noexcept { std::cout << "move constructor" << std::endl; }
    };
    
    int main() {
      std::vector<A> v;
      for (int i = 0; i < 10; i++) {
        A a;
        v.push_back(a);
      }
    
      return 0;
    }
    
    

    noexcept move constructor对性能的影响

    这篇文章C++ NOEXCEPT AND MOVE CONSTRUCTORS EFFECT ON PERFORMANCE IN STL CONTAINERS介绍了noexcept move constructor对耗时以及内存的影响,这里不重复赘述了,感兴趣的可以自己试一下。

    image

    参考资料:

    (完)

    朋友们可以关注下我的公众号,获得最及时的更新:

    image

  • 相关阅读:
    LeetCode15 3Sum
    LeetCode10 Regular Expression Matching
    LeetCode20 Valid Parentheses
    LeetCode21 Merge Two Sorted Lists
    LeetCode13 Roman to Integer
    LeetCode12 Integer to Roman
    LeetCode11 Container With Most Water
    LeetCode19 Remove Nth Node From End of List
    LeetCode14 Longest Common Prefix
    LeetCode9 Palindrome Number
  • 原文地址:https://www.cnblogs.com/zhangyachen/p/14077267.html
Copyright © 2011-2022 走看看