關於C++11中移動語義的一個問題?何時移動構造函數會被調用?

遇到一個不理解的問題:

#include &

using namespace std;

class A
{
public:
A(){}
A operator = (const A) {cout&<&<"move"&<&

輸出只有一個move

我這裡不太明白的是這一句

A a1 = A(); // 這裡調用的到底是哪一個構造函數?

按照我的理解,應該是首先構造A()的匿名對象,而這是一個右值,接下來應調用的是

A operator = (const A)

在完成a1的構造

可是從輸出的結果來看,他調用額並不是這個構造函數

那麼他調用的到底是哪一個構造函數?


首先,以下 3 行是等價的,都是調用預設構造函數:

A a1 = A();
A a1{};
A a1;

而這樣:

A a3 = a1;

則會調用複製構造函數。


以下分析基於working draft n4296 http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4296.pdf

A a1 = A();

從語法上看這是一個init-declarator: declarator initializer(opt)

declarator : A a1

initializer : = A()

使用的規則:

initializer: braced-or-equal-initializer

braced-or-equal-initializer: = initializer-clause

initializer-clause: assignment-expression

根據標準中8.5節裡面的說明

15 The initialization that occurs in the = form of a brace-or-equal-initializer or condition (6.4), as well as in argument passing, function return, throwing an exception (15.1), handling an exception (15.3), and aggregate member initialization (8.5.1), is called copy-initialization. [ Note: Copy-initialization may invoke a move (12.8). — end note ]

這裡的initializationcopy-initialization

根據標準中17.6.1處的描訴

If the initialization is direct-initialization, or if it is copy-initialization where the cv-unqualified version of the source type is the same class as, or a derived class of, the class of the destination, constructors are considered. The applicable constructors are enumerated (13.3.1.3), and the best one is chosen through overload resolution (13.3). The constructor so selected is called to initialize the object, with the initializer expression or expression-list as its argument(s). If no constructor applies, or the overload resolution is ambiguous, the initialization is ill-formed.

目標類型是class type, source類型是同樣的類型, 將會使用構造函數來初始化a1, 對應的expression-list將會作為構造函數的參數.

所以A a1 = A(); 的初始化過程是把右側表達式作為參數調用A的對應的構造函數.

實際上也就是先構造出一個temporary object, 然後用這個臨時對象初始化a1, 當A有move constructor的時候就是調用move constructor, 否則就調用copy constructor.

但是, 用g++測試, -O0下, 依然發現只調用了一次default constructor, 原因在於Copy elision, 簡單說就是在特定情況下, 編譯器被允許優化掉copy- and move-constructors, 即使對應的copy- and move-constructor, destructor有可觀測到的副作用. 所以A a1 = A() 就被優化為直接調用一次default constructor來初始化a1. 用g++編譯的時候加上-fno-elide-constructors可以禁止掉g++的這個優化, 從而明確地看到程序會調用一次default constructor, 一次move constructor, 兩次destructor(臨時對象和a1).

關於copy-elision, 見標準的12.8節31處的說明

31 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. 122 This elision of copy/move operations, called copy elision, is permitted in the following circumstances (which may be combined to eliminate multiple copies):

此處具體是符合31.3的情況:

31.3 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

值得指出的是, 在c++17做了調整

具體參考n4606的8.6節的17.6.1處的說明

If the initializer expression is a prvalue and the cv-unqualified version of the source type is the same class as the class of the destination, the initializer expression is used to initialize the destination object. [ Example: T x = T(T(T())); calls the T default constructor to initialize x. — end example ]

a1直接用A()進行初始化, 沒有copy- or move-constructor的調用


(Guarenteed) copy elision


反彙編分析下題主的程序 環境:VS2015

先說結論

A a1=A()直接優化為 A a1;

A a2=std::move(A());//開始用A()構造一個臨時對象,接著move使其成為右值,

接著將調用

A(const A){cout&<&<"move"&<&

A a1 = A();
00392578 lea ecx,[a1]
0039257B call A::A (039129Eh) //調用的是默認構造函數 A a2 = std::move(A());
00392580 lea ecx,[ebp-0E1h]
00392586 call A::A (039129Eh)
0039258B push eax
0039258C call std::move& (03913B6h)
00392591 add esp,4
00392594 push eax
00392595 lea ecx,[a2]
00392598 call A::A (03911B3h)

分析完畢~


當函數參數是右值的時候,自然就調用「移動版」重載函數,所以移動構造函數是在給定的參數對象是一個右值時被調用。

但是你這裡比較特殊的是,A a1 = A()的語義並不是構造一個臨時對象然後賦值。

實際上一般在對象初始化的時候,即使你使用「=」,也並不是表示調用operator=,而是調用拷貝構造函數。

而你這裡更加特殊的是,A a1 = A()不是先構造一個臨時對象然後調用拷貝構造,而是直接使用默認構造函數構造對象。也就是這裡完全等效於A a1。

你想像中的效果應該是這樣的:

A a1;
a1 = A();


《深度探索C++對象模型》中

2.3 程序轉化語意學 有提到這部分的語意。手打摘抄一下。

顯式的初始化操作 (Explicit Initializtion)

已知有這樣的定義

x x0;

下面的三個定義,每一個都明顯地以x0來初始化其class object

void foo_bar() {
X x1(x0);
X x2 = x0;
X x3 = X(x0);
}

必要的程序轉化有兩個階段:

1. 重寫每個定義,其中的初始化操作會被剝奪。

2. class的copy constructor調用會被安插就去。

.

.

以及

在編譯器層面做優化 (Optimization at the Compiler Level)

P.70(略)

提到了可能剔除copy constructor的優化(NRV)。

.

閱讀過這部分內容以後,對class object初始化的行為應該會有更深的理解。


你好!我這兩天也正在學習,碰到了類似的問題。我說說我的想法,不對的請大家指正

A a1 = A(); //這是聲明並初始化,因此調用拷貝構造函數

A a2;//這是聲明,執行了默認的無參構造函數,如果讓無參構造函數=delete,這句話編譯無法通過
a2 = a1; //這是賦值,調用的是拷貝賦值函數

含有關於拷貝構造函數和移動構造函數,我舉個栗子(野生小板栗):

在這我插一句話,由於編譯器的原因,我試了VS2013和CentOS7 上的g++,結果可能不一樣,我都說一下吧(都叫demo.cpp):

在VS2013上

#include&
#include&
#include&
using namespace std;

class myClass
{
string *m_ps;
public:
myClass()=default;
myClass(string *ps): m_ps(ps){cout&<&<"now, we are in the basic constructor!"&<&

運行結果如下:

a: we wang to test basic constructor by string *

now is in the basic constructor!

b: we want to test copy constructor

now is in the myClass(myClass p)

c: we want to test move constructor

now is in the basic constructor!

now is in the myClass(myClass p)

now is in the ~myClass

haha

now is in the ~myClass

now is in the ~myClass

now is in the ~myClass

P.S. VS2013上貌似不支持定義移動構造函數的時候使用noexcept關鍵字。

在CentOS7 上的g++,編譯命令如下:

g++ -o demo -std=c++11 -fno-elide-constructors demo.cpp

因為如果直接編譯,不指定使用c++11規則,則不會用c++11標準,然後各種錯誤就來了

正如那個匿名用戶說的(第一次使用知乎,沒找到樓層號,就暫且這樣稱呼這位大神吧),-fno-elide-constructors 才會使用移動構造函數,而不會被編譯器優化

我測試的代碼如下(其實類定義是一樣的,關鍵在使用移動構造函數那兒有點不一樣)

#include&
#include&
#include&
using namespace std;

class myClass
{
string *m_ps;
public:
myClass()=default;
myClass(string *ps): m_ps(ps){cout&<&<"now, we are in the basic constructor!"&<&

結果如下:

a: we wang to test basic constructor by string *

now, we are in the basic constructor!

b: we want to test copy constructor

now , we are in the myClass(myClass p)

c: we want to test move constructor

now, we are in the basic constructor!

now, we are in the myClass(myClass p)

now, we are in the ~myClass

haha

now, we are in the ~myClass

now, we are in the ~myClass

now, we are in the ~myClass

Ps 第一次在知乎上回答,希望大家多多指正!


在vs2010下幫你測試了一下。。

a1調用的是A(){}構造函數。

a2的時候行為如下:

首先執行std::move()內的A(),調用A()構造一個新的匿名A

然後調用std::move();

最後才是調用operator =(consta A),輸出一個move。

移動語義本身是為了解決無法進行右值引用的問題才被加入到c++0x中的,這一語義的添加實際上是為了更好地優化內存分配才進行的。

http://blog.csdn.net/hikaliv/article/details/4541429中提到:

  1. 移動構造重載函數和移動賦值算符(assignment operators:=、^=、+=,etc.)重載函數絕不會隱式聲明,必須自己定義。

  2. 默認構造函數會被用戶自己顯式定義的構造函數壓制,包括用戶自定義複製構造函數和移動構造函數。因故若用戶已自定義複製和移動構造函數,且需要無參構造函數時,也需要自己定義。

  3. 隱式複製構造函數會被用戶自己顯式定義的複製構造函數覆蓋,而不是自定義的移動構造函數。

  4. 隱式複製賦值重載函數會被用戶自己顯式定義的複製賦值重載函數覆蓋,而不是自定義的移動賦值重載函數。

其實現原理在同一作者的另一篇文章中(http://blog.csdn.net/hikaliv/article/details/4541419)也有闡述。

希望對你有幫助。我對這裡也不是特別明白。


推薦閱讀:

工廠設計模式有什麼用?
遊戲設計模式(一) 序言:架構,性能與遊戲
7個有益的編程習慣
在 C++11 中,如何為匿名的結構體添加構造函數?
《自頂向下方法》筆記 · 編程作業2 · UDPping程序

TAG:編程語言 | 編程 | C | C11 |