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中的几个成员函数:
- 函数在c++98版本中已经被声明为
# 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对耗时以及内存的影响,这里不重复赘述了,感兴趣的可以自己试一下。
参考资料:
- When to Use noexcept And When to Not
- Does noexcept improve performance?
- EffectiveModernCppChinese Item14
- C++11的noexcept标识符与操作符应如何正确使用?
朋友们可以关注下我的公众号,获得最及时的更新:
c++ 从vector扩容看noexcept应用场景的更多相关文章
- C++ STL vector扩容原理分析
扩容特点: 1)新增元素:vector通过一个连续的数组存放元素,如果集合已满,在新增数据的时候,就要分配一块更大的内存,将原来的数据复制过来,释放之前的内存,在插入新增的元素: 2)对vector的 ...
- c++11-17 模板核心知识(十一)—— 编写泛型库需要的基本技术
Callables 函数对象 Function Objects 处理成员函数及额外的参数 std::invoke<>() 统一包装 泛型库的其他基本技术 Type Traits std:: ...
- Ngnix技术研究系列1-通过应用场景看Nginx的反向代理
随着我们业务规模的不断增长,整个系统规模由两年前的几十台服务器,井喷到现在2个数据中心,接近400台服务器,上百个WebApi站点,上百个域名. 这么多的WebApi站点这么多的域名,管理和维护成本很 ...
- Nginx技术研究系列1-通过应用场景看Nginx的反向代理
随着我们业务规模的不断增长,整个系统规模由两年前的几十台服务器,井喷到现在2个数据中心,接近400台服务器,上百个WebApi站点,上百个域名. 这么多的WebApi站点这么多的域名,管理和维护成本很 ...
- Java集合(3):Vector && Stack
一.Vector介绍 Vector可以实现可增长的动态对象数组.与数组一样,它包含可以使用整数索引进行访问的组件.不过,Vector的大小是可以增加或者减小的,以便适应创建Vector后进行添加或者删 ...
- 一份非常值得一看的Java面试题
包含的模块 本文分为十九个模块,分别是: Java 基础.容器.多线程.反射.对象拷贝.Java Web .异常.网络.设计模式.Spring/Spring MVC.Spring Boot/Sprin ...
- JDK1.8源码阅读系列之三:Vector
本篇随笔主要描述的是我阅读 Vector 源码期间的对于 Vector 的一些实现上的个人理解,用于个人备忘,有不对的地方,请指出- 先来看一下 Vector 的继承图: 可以看出,Vector 的直 ...
- STL源码剖析-vector
STL(Standard Template Library) C++标准模板库,acm选手一定对它不陌生,除了算法,那几乎是“吃饭的家伙了”.我们使用库函数非常方便,且非常高效(相对于自己实现来说). ...
- 分布式缓存集群方案特性使用场景(Memcache/Redis(Twemproxy/Codis/Redis-cluster))优缺点对比及选型
分布式缓存集群方案特性使用场景(Memcache/Redis(Twemproxy/Codis/Redis-cluster))优缺点对比及选型 分布式缓存特性: 1) 高性能:当传统数据库面临大规模 ...
随机推荐
- Shiro入门学习---使用自定义Realm完成认证|练气中期
写在前面 在上一篇文章<shiro认证流程源码分析--练气初期>当中,我们简单分析了一下shiro的认证流程.不难发现,如果我们需要使用其他数据源的信息完成认证操作,我们需要自定义Real ...
- JSON.stringify 的使用
一.作用:这个函数的作用主要是为了序列化对象.就是把原来是对象的类型转换成字符串类型(json格式的String类型). 二.语法:JSON.stringify(value[, replacer][, ...
- C语言中i++和++i的区别
这一篇更详细: 转载:https://blog.csdn.net/Bug_fuck/article/details/85229229 C语言中++i和i++是有区别的!快速理解的话就是用一句话概括:1 ...
- I2C总线的Arduino库函数
I2C总线的Arduino库函数 I2C即Inter-Integrated Circuit串行总线的缩写,是PHILIPS公司推出的芯片间串行传输总线.它以1根串行数据线(SDA)和1根串行时钟线(S ...
- 跟着动画学习 TCP 三次握手和四次挥手
TCP三次握手和四次挥手的问题在面试中是最为常见的考点之一.很多读者都知道三次和四次,但是如果问深入一点,他们往往都无法作出准确回答. 本篇尝试使用动画来对这个知识点进行讲解,期望读者们可以更加简单地 ...
- Netty之网络编程数据编码
一.概况 我们在进行网络编程中会把各种数据转换为byte数据以便能在网络上传输,最常见的网络字节序--Little-Endian和Big-Endian,也让好多初进网络编程的新手摸不着头脑,还有按位或 ...
- 记录小坑-tp5 使用模型select查询
场景: 使用模型去select查询后进行业务处理 再进行 saveAll 提示缺少更新条件 坑点:此时取出的数据结构是 query对象 { array:[ xxxx => xxx ] }: sa ...
- git fatal: Path 'XXX' is in submodule 'XXX'错误
easyswoole项目的 vendor/easyswoole/socket/这个项目怎么都无法添加到git目录里面. 报错: Administrator@PhpServer MINGW64 /z/w ...
- 多测师讲解python _练习题002_高级讲师肖sir
# 1.求出1/1+1/3+1/5--+1/99的和 # 2.用循环语句,计算2-10之间整数的循环相乘的值. # 3.用for循环打印九九乘法表 # 4.求每个字符串中字符出现的个数如:hellow ...
- day20 Pyhton学习 面向对象-成员
一.类的成员 class 类名: # 方法 def __init__(self, 参数1, 参数2....): # 属性变量 self.属性1 = 参数1 self.属性2 = 参数2 .... # ...