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識別符號與操作符應如何正確使用?
(完)
朋友們可以關注下我的公眾號,獲得最及時的更新: