class類
復制/移動省略class.copy.elision
類復制省略 (copy elision)
當滿足特定條件時,即使所選對象的構造函數和/或析構函數有副作用,實現也被允許省略從相同類型(忽略 cv 限定符)的源對象創建類對象。
在這種情況下,實現將省略的初始化的源和目標視為引用同一對象的兩種不同方式。如果所選構造函數的第一個參數是對對象類型的右值引用,則該對象的析構發生在目標對象本應被析構的時刻;否則,析構發生在未進行優化時兩個對象本應被析構的較晚時刻。
[注 1]: 因為只有一個對象被析構而不是兩個,并且一個對象的創建被省略了,所以對于每個構造的對象仍然只有一個對象被析構。——尾注
這種對象創建的省略,稱為復制省略 (copy elision),在以下情況下是允許的(這些情況可以組合以消除多次復制):
-
在返回語句中 (return statement): 在具有類返回類型的函數中,當表達式是具有自動存儲期的非 volatile 對象
o
(不是函數參數,也不是由處理程序的異常聲明引入的變量)的名稱時,可以通過直接將o
構造到函數調用的結果對象中來省略結果對象的復制初始化。 -
在 throw 表達式中 (throw-expression): 在 throw 表達式中,當操作數是具有自動存儲期的非 volatile 對象
o
(不是函數參數,也不是由處理程序的異常聲明引入的變量)的名稱,且該對象所屬的作用域不包含與 try 塊關聯的最內層復合語句(如果存在)時,可以通過直接將o
構造到異常對象中來省略異常對象的復制初始化。 -
在協程中 (coroutine): 在協程中,如果程序的語義除了執行參數拷貝對象的構造函數和析構函數之外保持不變,則可以省略協程參數的拷貝,并將對該拷貝的引用替換為對相應參數的引用。
-
在異常處理器的異常聲明中:當處理程序的異常聲明聲明了一個對象
o
時,如果程序的語義除了執行異常聲明所聲明的對象的構造函數和析構函數之外保持不變,則可以通過將異常聲明視為異常對象的別名來省略o
的復制初始化。[注 2]: 不能從異常對象進行移動,因為它總是左值。——尾注
在要求常量表達式的上下文中計算表達式時,以及在常量初始化時,不允許進行復制省略。
[注 3]: 如果同一表達式在另一個上下文中求值,仍有可能執行復制省略。——尾注
[示例 1]:
class Thing {
public:Thing();~Thing();Thing(const Thing&);
};
Thing f() {Thing t;return t; // 允許省略將 t 復制/移動到 f() 的結果對象
}
Thing t2 = f(); // 允許省略將 f() 的返回值復制/移動到 t2struct A {void* p;constexpr A() : p(this) {}
};
constexpr A g() {A loc;return loc; // 常量求值上下文: 不允許省略!
}
constexpr A a; // 正確,a.p 指向 a
constexpr A b = g(); // 錯誤: b.p 將是懸垂指針 ([expr.const])
void h() {A c = g(); // 正確, c.p 可以指向 c 或是懸垂的
}
此例中,省略標準可以消除將具有自動存儲期的對象
t
復制到函數調用f()
的結果對象(即非局部對象t2
)中。實際上,t
的構造可以被視為直接初始化t2
,并且該對象的析構將發生在程序退出時。向Thing
添加移動構造函數具有相同的效果,但被省略的是從具有自動存儲期的對象到t2
的移動構造。——尾例
[示例 2]:
class Thing {
public:Thing();~Thing();Thing(Thing&&);
private:Thing(const Thing&); // 復制構造函數私有,強制使用移動(如果可能)
};
Thing f(bool b) {Thing t;if (b)throw t; // 正確, 使用 Thing(Thing&&) (或省略) 來拋出 treturn t; // 正確, 使用 Thing(Thing&&) (或省略) 來返回 t
}
Thing t2 = f(false); // 正確, 沒有額外的復制/移動操作, t2 由對 f 的調用構造struct Weird {Weird();Weird(Weird&); // 注意:非常量左值引用復制構造函數
};
Weird g(bool b) {static Weird w1; // 靜態存儲期Weird w2; // 自動存儲期if (b)return w1; // 正確, 使用 Weird(Weird&) (w1 是左值)elsereturn w2; // 錯誤: 在此上下文中 w2 是 xvalue (將亡值),但 Weird 沒有接受右值的構造函數
}int& h(bool b, int i) {static int s;if (b)return s; // 正確,返回靜態變量的左值引用elsereturn i; // 錯誤: i 是自動變量,在此上下文中是 xvalue,但函數返回左值引用
}decltype(auto) h2(Thing t) {return t; // 正確, t 是 xvalue, h2 的返回類型推導為 Thing (值類型)
}
decltype(auto) h3(Thing t) {return (t); // 正確, (t) 是 xvalue, h3 的返回類型推導為 Thing&& (右值引用)
}
——尾例
[示例 3]:
template <class T> void g(const T&);
template <class T> void f() {T x; // 外層作用域對象try {T y; // 內層作用域對象try {g(x);} catch (...) {if (/*...*/)throw x; // 不會移動 (x 屬于包含 try 塊的作用域,不符合 1.2 省略條件)throw y; // 移動 (y 屬于不包含 try 塊的內層作用域,符合 1.2 省略條件。若省略則直接構造異常對象)}g(y);} catch (...) {g(x);g(y); // 錯誤: y 不在作用域內}
}
——尾例
核心總結:
此標準條款定義了 C++ 中的復制/移動省略 (Copy/Move Elision) 規則,這是編譯器為了優化性能而避免不必要的對象復制或移動的關鍵機制。
- 本質與效果:
- 允許編譯器在特定條件下,完全省略從一個對象(源)創建另一個同類型對象(目標)的操作。
- 被省略后,源和目標被視為同一個對象的兩種引用方式。
- 析構時機:若使用移動構造函數,則在目標對象該析構時析構;若使用復制構造函數,則在源和目標原該析構的較晚時刻析構。最終效果是只構造和析構了一個對象。
- 允許省略的場景 (可組合使用):
- 命名返回值優化 (NRVO): 函數返回局部非 volatile 自動存儲期對象(非參數/異常聲明變量)的名稱時 (
return local_var;
),可省略將local_var
復制/移動到函數返回值的操作,直接在返回值位置構造。 - Throw 表達式優化: 拋出局部非 volatile 自動存儲期對象(非參數/異常聲明變量,且其作用域不包含最內層 try 塊)的名稱時,可省略將其復制/移動到異常對象的操作,直接在異常對象位置構造。
- 協程參數省略: 在協程中,可省略對協程參數的拷貝。
- 異常處理器別名: 在
catch
塊的異常聲明中 (catch (Type obj)
),可省略對異常對象的拷貝,直接將obj
視為異常對象的別名。
- 命名返回值優化 (NRVO): 函數返回局部非 volatile 自動存儲期對象(非參數/異常聲明變量)的名稱時 (
- 禁止省略的場景:
- 常量表達式求值: 在要求常量表達式的上下文中(如
constexpr
變量初始化、constexpr
函數內的 return)。 - 常量初始化 (靜態初始化): 在靜態存儲期對象的常量初始化過程中。
- 常量表達式求值: 在要求常量表達式的上下文中(如
- 關鍵點與影響:
- 性能提升: 省略操作避免了潛在的昂貴復制/移動構造函數和析構函數調用,顯著提升性能。
- 副作用容忍: 即使被省略的構造函數或析構函數有可觀測的副作用(如打印日志),編譯器仍被允許進行省略(這是
as-if
規則的例外)。 - 移動構造的特殊性: 條款明確說明了當省略涉及移動構造函數時析構發生的時機。
- 標準要求 (C++17起): 對于 NRVO (場景 1) 和純右值初始化,滿足條件時編譯器必須進行省略(稱為“強制省略”或“保證的復制省略”)。其他場景(如 throw 優化)是允許但不強制的。
- 作用域與生存期: 示例 2 和 3 展示了對象的作用域(特別是相對于
try
塊的位置)如何影響省略的可行性,以及在異常處理中對象生存期的微妙問題。
簡而言之: 此條款賦予編譯器權力,在特定且定義明確的場景下(尤其是函數返回局部對象和拋出局部對象時),可以完全繞過復制或移動構造函數,直接“復用”源對象作為目標對象,從而生成更高效的代碼。理解這些規則對于編寫高性能 C++ 代碼和避免不必要的 std::move
(如之前文章所述)至關重要。
原文翻譯
11.9 Initialization
11.9 初始化類.init
11.9.6 Copy/move elision
11.9.6 復制/移動省略[class.copy.elision]??[類復制.省略]
When certain criteria are met, an implementation is allowed to omit the creation of a class object from a source object of the same type (ignoring cv-qualification), even if the selected constructor and/or the destructor for the object have side effects.
當滿足某些條件時,實現為 允許省略 Class Object 的創建 相同類型的源對象(忽略 cv 限定), 即使所選構造函數和/或 對象的析構函數具有 副作用 。
In such cases, the implementation treats the source and target of the omitted initialization as simply two different ways of referring to the same object.
在這種情況下,實現將省略的初始化的 source 和 target 視為引用同一對象的兩種不同方式 。
If the first parameter of the selected constructor is an rvalue reference to the object’s type, the destruction of that object occurs when the target would have been destroyed; otherwise, the destruction occurs at the later of the times when the two objects would have been destroyed without the optimization.
如果所選構造函數的第一個參數是對象類型的右值引用,則當目標已銷毀時,將銷毀該對象;否則,銷毀發生在沒有優化的情況下銷毀兩個對象的時間 。
[Note?1:
Because only one object is destroyed instead of two, and the creation of one object is omitted, there is still one object destroyed for each one constructed.
—?end note]
[ 注?1:
因為只有一個對象被銷毀而不是兩個,并且省略了一個對象的創建,所以每個構建的對象仍然有一個對象被銷毀 。
This elision of object creation, called copy elision, is permitted in the following circumstances (which may be combined to eliminate multiple copies):
這種對象創建的省略稱為 復制省略 / 允許在 以下情況(可能會與 消除多個拷貝):
-
in a return statement ([stmt.return]) in a function with a class return type, when the expression is the name of a non-volatile object o with automatic storage duration (other than a function parameter or a variable introduced by the exception-declaration of a handler ([except.handle])), the copy-initialization of the result object can be omitted by constructing o directly into the function call’s result object;
在具有類 return 類型的函數的 return 語句 ([stmt.return]) 中,當表達式是具有自動存儲持續時間的非易失性對象的名稱 o 時(函數參數或由 a 的異常聲明引入的變量除外) handler ([除外。handle])),可以通過將 o 直接構造到函數調用的 result 對象中來省略 result 對象的復制初始化;
-
in a throw-expression ([expr.throw]), when the operand is the name of a non-volatile object o with automatic storage duration (other than a function parameter or a variable introduced by the exception-declaration of a handler) that belongs to a scope that does not contain the innermost enclosing compound-statement associated with a try-block (if there is one), the copy-initialization of the exception object can be omitted by constructing o directly into the exception object;
在 throw 表達式 ([expr.throw]),當作數是具有自動存儲持續時間的非易失性對象 o 的名稱(函數參數或處理程序的異常聲明引入的變量除外)時,該對象屬于不包含最內層封閉復合語句的作用域 與 try 塊 (如果有)相關聯,可以通過將 o 直接構造到 Exception 對象中來省略 Exception 對象的復制初始化;
-
in a coroutine, a copy of a coroutine parameter can be omitted and references to that copy replaced with references to the corresponding parameter if the meaning of the program will be unchanged except for the execution of a constructor and destructor for the parameter copy object;
在協程中,如果程序的含義保持不變,則除了執行參數 copy 對象的構造函數和析構函數外,可以省略協程參數的副本,并將對該副本的引用替換為對相應參數的引用;
-
when the exception-declaration of a handler ([except.handle]) declares an object o, the copy-initialization of o 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.
當處理程序 ([except.handle]) 的異常聲明聲明對象 o 時,如果程序的含義保持不變,則可以通過將異常聲明視為異常對象的別名來省略 o 的復制初始化,但異常聲明聲明的對象除外。
[Note?2:
There cannot be a move from the exception object because it is always an lvalue.
—?end note]
[ 注?2:
不能從異常對象移動,因為它始終是左值 。
[Note?3:
It is possible that copy elision is performed if the same expression is evaluated in another context.
—?end note]
[ 注?3:
如果在另一個上下文中計算相同的表達式,則可能會執行復制省略 。
[Example?1:
class Thing {
public:Thing();~Thing();Thing(const Thing&);
};
Thing f() {Thing t;return t; // 允許省略將 t 復制/移動到 f() 的結果對象
}
Thing t2 = f(); // 允許省略將 f() 的返回值復制/移動到 t2struct A {void* p;constexpr A() : p(this) {}
};
constexpr A g() {A loc;return loc; // 常量求值上下文: 不允許省略!
}
constexpr A a; // well-formed, a.p points to a ;正確,a.p 指向 a
constexpr A b = g(); // error: b.p would be dangling ([expr.const]);錯誤: b.p 將是懸垂指針 ([expr.const])
void h() {A c = g(); // well-formed, c.p can point to c or be dangling;正確, c.p 可以指向 c 或是懸垂的
}
—?end example]
這里的省略標準可以消除將具有自動存儲持續時間的對象 t 復制到函數調用 f() 的結果對象中,即非本地對象 t2。
實際上,t 的構造可以看作是直接初始化 t2,并且該對象的銷毀將在程序退出時發生 。
向 Thing 添加移動構造函數具有相同的效果,但省略了從具有自動存儲持續時間的對象到 t2 的移動構造 。
[Example?2:
class Thing {
public:Thing();~Thing();Thing(Thing&&);
private:Thing(const Thing&); // 復制構造函數私有,強制使用移動(如果可能)
};
Thing f(bool b) {Thing t;if (b)throw t; //OK, Thing(Thing&&) used (or elided) to throw t; 正確, 使用 Thing(Thing&&) (或省略) 來拋出 treturn t; // OK, Thing(Thing&&) used (or elided) to return t;正確, 使用 Thing(Thing&&) (或省略) 來返回 t
}
Thing t2 = f(false); // OK, no extra copy/move performed, t2 constructed by call to f
;正確, 沒有額外的復制/移動操作, t2 由對 f 的調用構造struct Weird {Weird();Weird(Weird&); // 注意:非常量左值引用復制構造函數
};
Weird g(bool b) {static Weird w1; // 靜態存儲期Weird w2; // 自動存儲期if (b)return w1; // OK, uses Weird(Weird&);正確, 使用 Weird(Weird&) (w1 是左值)elsereturn w2; // error: w2 in this context is an xvalue;錯誤: 在此上下文中 w2 是 xvalue (將亡值),但 Weird 沒有接受右值的構造函數
}int& h(bool b, int i) {static int s;if (b)return s; // OK;正確,返回靜態變量的左值引用elsereturn i; // error: i is an xvalue;錯誤: i 是自動變量,在此上下文中是 xvalue,但函數返回左值引用
}decltype(auto) h2(Thing t) {return t; // OK, t is an xvalue and h2's return type is Thing;正確, t 是 xvalue, h2 的返回類型推導為 Thing (值類型)
}
decltype(auto) h3(Thing t) {return (t); // OK, (t) is an xvalue and h3's return type is Thing&&;正確, (t) 是 xvalue, h3 的返回類型推導為 Thing&& (右值引用)
}
—?end example]
[Example?3:
template <class T> void g(const T&);
template <class T> void f() {T x; // 外層作用域對象try {T y; // 內層作用域對象try {g(x);} catch (...) {if (/*...*/)throw x; //does not move; 不會移動 (x 屬于包含 try 塊的作用域,不符合 1.2 省略條件)throw y; // moves;移動 (y 屬于不包含 try 塊的內層作用域,符合 1.2 省略條件。若省略則直接構造異常對象)}g(y);} catch (...) {g(x);g(y); // error: y is not in scope;錯誤: y 不在作用域內}
}
—?end example]