C++ Copy Elision

仗劍發表於2019-05-11

故事得從 copy/move constructor 說起:

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 ]

上面這段文字來自 C++11 Standard 中 “12 Special member functions”,關於什麼時候使用 copy/move constructor 什麼時候使用 copy/move assignment operator,在 “12.8 Copying and moving class objects” 中第一段有詳細的說明:

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).

也就是說:在初始化、函式引數傳遞和函式值返回的時候將會使用到 copy/move constructor,而在賦值的時候才會使用到 copy/move assignment operator

一、copy/move constructor

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).

Xcopy constructor 是一個非模板建構函式,該函式的第一個引數必須是 X&const X&volatile X& 或者 const volatile X&,如果還有其他引數,其他引數必須有預設值

為了簡化問題,這裡我們不討論 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
";         }
    X& operator=(const X&) { std::cout << "copy assignment operator
"; }
};

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 所說,上面程式碼的行為應該是和註釋一樣,於是我們編譯並執行試試:

$ g++ a.cpp -o a
$ ./a
default constructor
copy constructor
copy constructor
copy assignment operator
destructor
destructor
destructor

結果的確是和預期的一致,那麼再來看看需要使用 copy constructor 的另外一種情況 “function value return”,這裡不涉及利用函式返回值初始化另一個物件的情況,只是單純的呼叫函式:

#include <iostream>

struct X {
    X()  { std::cout << "default constructor" << std::endl; }
    ~X() { std::cout << "destructor"          << std::endl; }

    X(const X&)            { std::cout << "copy constructor
";         }
    X& operator=(const X&) { std::cout << "copy assignment operator
"; }
};

X f() {
    return X();
}

int main() {
    f();
    return 0;
}

編譯並執行,其結果如下:

$ g++ a.cpp -o a
$ ./a
default constructor
destructor

預期的那次 copy constructor 呼叫並沒有出現。這裡不得不說到一個編譯器優化:return value optimization

二、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 Elision

讓我們更近一步,如果細心的話可能已經發現,在上面貼出來的省略 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
";         }
    X& operator=(const X&) { std::cout << "copy assignment operator
"; }
};

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
";         }
    X& operator=(const X&) { std::cout << "copy assignment operator
"; }
};

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

相關文章