当前位置: 首页 > 编程笔记 >

C++ vector扩容解析noexcept应用场景

刁钧
2023-03-14
本文向大家介绍C++ vector扩容解析noexcept应用场景,包括了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到新容器内:

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

申请新空间时出现异常:旧vector还是保持原有状态,抛出的异常交由用户自己处理。

copy元素时出现异常:所有已经被copy的元素利用元素的析构函数释放,已经分配的空间释放掉,抛出的异常交由用户自己处理。

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

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

利用move的交换类资源所有权的特性,使用vector扩容效率大大提高,但是当发生异常时怎么办:
原有容器的状态已经被破坏,有部分元素的资源已经被偷走。若要恢复会极大增加代码的复杂性和不可预测性。所以只有当vector中元素的move constructor是noexcept时,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 trait和iterator 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 trait和iterator 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对耗时以及内存的影响,这里不重复赘述了,感兴趣的可以自己试一下。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持小牛知识库。

 类似资料:
  • 主要内容:1 Vector的概述,2 Vector的源码解析,2.1 主要类属性,2.2 构造器与初始化容量,2.3 add方法与扩容机制,2.4 addAll方法,2.5 remove方法,2.6 get方法,2.7 set方法,2.8 clone方法,2.9 序列化,3 迭代器,3.1 Enumeration迭代器的概述,3.2 Enumeration迭代器的实现,3.3 分析Enumeration迭代器的死循环,,基于JDK1.8对Java中的Vector集合的源码进行了深度解析,包括各种

  • 本文向大家介绍c++ vector 常用函数示例解析,包括了c++ vector 常用函数示例解析的使用技巧和注意事项,需要的朋友参考一下 c++ vector 常用函数 Just like arrays, vectors use contiguous storage locations for their elements, which means that their elements can

  • 本文向大家介绍简单讲解c++ vector,包括了简单讲解c++ vector的使用技巧和注意事项,需要的朋友参考一下 在c++中,vector是一个十分有用的容器。 作用:它能够像容器一样存放各种类型的对象,简单地说,vector是一个能够存放任意类型的动态数组,能够增加和压缩数据。 vector在C++标准模板库中的部分内容,它是一个多功能的,能够操作多种数据结构和算法的模板类和函数库。 特别

  • 本文向大家介绍C++中vector容器使用详细说明,包括了C++中vector容器使用详细说明的使用技巧和注意事项,需要的朋友参考一下 在c++中,vector是一个十分有用的容器,下面通过本文给大家介绍C++中vector容器使用详细说明,具体介绍如下所示 1. 在C++中的详细说明 vector是C++标准模板库中的部分内容,它是一个多功能的,能够操作多种数据结构和算法的模板类和函数库。 ve

  • 本文向大家介绍C++(STL库)之顺序容器vector的使用,包括了C++(STL库)之顺序容器vector的使用的使用技巧和注意事项,需要的朋友参考一下 一、特点 ①总的来说:可变大小数组。支持快速随机访问。在尾部之外的位置插入或删除元素可能很慢 ②元素保存在连续的内存空间中,因此通过下标取值非常快 ③在容器中间位置添加或删除元素非常耗时 ④一旦内从重分配,和原vector相关的指针,引用,迭代

  • 如果一个函数不能抛出异常,或者一个程序并没有接获某个函数所抛出的异常并进行处理,那么这个函数可以用新的noexcept关键字对其进行修饰,表示这个函数不会抛出异常或者抛出的异常不会被接获并处理。例如: extern "C" double sqrt(double) noexcept; // 永远不会抛出异常 vector my_computation(const vector& v) noe