故事得從 copy/move constructor 提及:ios
The default constructor (12.1), copy constructor and copy assignment operator (12.8), move constructor and move assignment operator (12.8), and destructor (12.4) are special member functions. [ Note: The implementation will implicitly declare these member functions for some class types when the program does not explicitly declare them. The implementation will implicitly define them if they are odr-used (3.2). See 12.1, 12.4 and 12.8. — end note ]express
上面這段文字來自 C++11 Standard 中 「12 Special member functions」,關於何時使用 copy/move constructor 何時使用 copy/move assignment operator,在 「12.8 Copying and moving class objects」 中第一段有詳細的說明:bash
A class object can be copied or moved in two ways: by initialization (12.1, 8.5), including for function argument passing (5.2.2) and for function value return (6.6.3); and by assignment (5.17). Conceptually, these two operations are implemented by a copy/move constructor (12.1) and copy/move assignment operator (13.5.3).ide
也就是說:在初始化、函數參數傳遞和函數值返回的時候將會使用到 copy/move constructor,而在賦值的時候纔會使用到 copy/move assignment operator函數
A non-template constructor for class X is a copy constructor if its first parameter is of type X&, const X&, volatile X& or const volatile X&, and either there are no other parameters or else all other parameters have default arguments (8.3.6).優化
類 X 的 copy constructor 是一個非模板構造函數,該函數的第一個參數必須是 X&、const X&、volatile X& 或者 const volatile X&,若是還有其餘參數,其餘參數必須有默認值code
爲了簡化問題,這裏咱們不討論 move constructor,也就是說,假設 gcc 版本爲 4.1.2 而且不支持 C++0x 提出的 move semantic。看看下面這段例子:對象
#include <iostream> struct X { X() { std::cout << "default constructor" << std::endl; } ~X() { std::cout << "destructor" << std::endl; } X(const X&) { std::cout << "copy constructor\n"; } X& operator=(const X&) { std::cout << "copy assignment operator\n"; } }; int main() { X a; // initialization, use default constructor X aa(a); // initialization, use copy constructor X aaa = a; // initialization, use copy constructor aa = a; // assignment, use copy assignment operator return 0; }
按 Standard 所說,上面代碼的行爲應該是和註釋同樣,因而咱們編譯並運行試試:ip
$ g++ a.cpp -o a $ ./a default constructor copy constructor copy constructor copy assignment operator destructor destructor destructor
結果的確是和預期的一致,那麼再來看看須要使用 copy constructor 的另一種狀況 「function value return」,這裏不涉及利用函數返回值初始化另外一個對象的狀況,只是單純的調用函數:ci
#include <iostream> struct X { X() { std::cout << "default constructor" << std::endl; } ~X() { std::cout << "destructor" << std::endl; } X(const X&) { std::cout << "copy constructor\n"; } X& operator=(const X&) { std::cout << "copy assignment operator\n"; } }; X f() { return X(); } int main() { f(); return 0; }
編譯並運行,其結果以下:
$ g++ a.cpp -o a $ ./a default constructor destructor
預期的那次 copy constructor 調用並無出現。這裏不得不說到一個編譯器優化:return value optimization
When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the constructor selected for the copy/move operation and/or the destructor for the object have side effects. In such cases, the implementation treats the source and target of the omitted copy/move operation as simply two different ways of referring to the same object, and the destruction of that object occurs at the later of the times when the two objects would have been destroyed without the optimization.126 This elision of copy/move operations, called copy elision, is permitted in the following circumstances (which may be combined to eliminate multiple copies):
— in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object (other than a function or catch-clause parameter) with the same cv-unqualified type as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function’s return value
— in a throw-expression, when the operand is the name of a non-volatile automatic object (other than a function or catch-clause parameter) whose scope does not extend beyond the end of the innermost enclosing try-block (if there is one), the copy/move operation from the operand to the exception object (15.1) can be omitted by constructing the automatic object directly into the exception object
— when a temporary class object that has not been bound to a reference (12.2) would be copied/moved to a class object with the same cv-unqualified type, the copy/move operation can be omitted by constructing the temporary object directly into the target of the omitted copy/move
— when the exception-declaration of an exception handler (Clause 15) declares an object of the same type (except for cv-qualification) as the exception object (15.1), the copy/move operation can be omitted by treating the exception-declaration as an alias for the exception object if the meaning of the program will be unchanged except for the execution of constructors and destructors for the object declared by the exception-declaration.
這裏注意第一段就好了:某些場景下,編譯器能夠省略一次 copy/move construction,無論有沒有 side effect。省略 copy/move construction 能帶來什麼 side effect 呢?copy/move constructor 裏的代碼不會執行(好比上面例子中的 cout 信息)。Standard 給出幾個省略 copy/move construction 的場景,場景一就是上面的狀況:
函數的 return 語句中的表達式是一個非 volatile 對象的名字,而且其 cv-unqualified type 和函數返回值的 cv-unqualified type 相同,此時能夠省略一次 copy/move construction。
那麼什麼是 cv-unqualified type 和 cv-qualified type 呢?若是有耐心的話,Standard 裏也是有講的,在第 3.9.3 節,這裏我就不貼原文了,簡單的說就是:"cv" 分別指的是 "const" 和 "volatile",cv-unqualified type 指的是沒有這兩個修飾符修飾的類型。能夠看看 Stack Overflow 上面的解釋,簡單精確:What does "cv-unqualified" mean in C++?
如今就可以解釋上面例子中的行爲了:函數返回值被保存到了一個臨時變量裏,而構造這個臨時變量調用的是類 X 的 copy/move constructor,傳入的參數是 return 語句後面的表達式,巧合的是,臨時變量和傳入參數的 cv-unqualified type 相同,所以 gcc 把此次 copy/move construction 省略掉了(或者說優化掉了),而在 copy/move constructor 中的 cout 代碼就天然不被執行了,這就是省略 copy/move construction 帶來的 side effect 吧。
gcc 提供了一個編譯選項:-fno-elide-constructors
,用它可以關閉 gcc 省略 copy/move construction 的默認行爲,因此,若是咱們這樣編譯代碼並運行的話,就可以看見指望看見的那次 copy/move construction 了:
$ g++ a.cpp -o a -fno-elide-constructors $ ./a default constructor copy constructor destructor destructor
關於 return value optimization,能夠看看維基百科:Return value optimization
讓咱們更近一步,若是細心的話可能已經發現,在上面貼出來的省略 copy/move construction 的條件裏還有這麼一條:
— when a temporary class object that has not been bound to a reference (12.2) would be copied/moved to a class object with the same cv-unqualified type, the copy/move operation can be omitted by constructing the temporary object directly into the target of the omitted copy/move
當用一個臨時對象初始化另外一個對象的時候,若是他們倆的 cv-unqualified type 相同,而且臨時對象沒有和任何引用綁定,那麼這次 copy/move construction 也是能夠省略的:
#include <iostream> struct X { X() { std::cout << "default constructor" << std::endl; } ~X() { std::cout << "destructor" << std::endl; } X(const X&) { std::cout << "copy constructor\n"; } X& operator=(const X&) { std::cout << "copy assignment operator\n"; } }; X f() { return X(); } int main() { X a = f(); return 0; }
若是咱們直接編譯運行的話,那兩次 copy/move construction 確定都被優化掉了:
$ g++ a.cpp -o a $ ./a default constructor destructor
若是加上 -fno-elide-constructors 這個選項:
$ g++ a.cpp -o a -fno-elide-constructors $ ./a default constructor copy constructor destructor copy constructor destructor destructor
爲了做對比,若是不用那個臨時變量初始化一個 X 對象,而是先把它賦值給一個 X 對象 a,而後用 a 來 copy initialize 一個 X 對象 b,那麼初始化 b 的那次 copy construction 是不會被省略的:
#include <iostream> struct X { X() { std::cout << "default constructor" << std::endl; } ~X() { std::cout << "destructor" << std::endl; } X(const X&) { std::cout << "copy constructor\n"; } X& operator=(const X&) { std::cout << "copy assignment operator\n"; } }; X f() { return X(); } int main() { X a = f(); X b = a; return 0; }
編譯並運行:
$ g++ a.cpp -o a $ ./a default constructor copy constructor destructor destructor
結果和預想的一致,copy elision 大體就解釋完了,有空能夠看看維基百科:Copy elision