定義與 ODR(單一定義規則)
定義 (Definitions) 是指完整定義了宣告所引入之實體的宣告。除了以下情況外,每個宣告都是定義:
- 沒有函式體的函式宣告
int f(int); // declares, but does not define f
extern const int a; // declares, but does not define a extern const int b = 1; // defines b
- 在類別定義內部的 非 inline (non-inline)(C++17 起) 靜態資料成員宣告
struct S { int n; // defines S::n static int i; // declares, but does not define S::i inline static int x; // defines S::x }; // defines S int S::i; // defines S::i
struct S { static constexpr int x = 42; // implicitly inline, defines S::x }; constexpr int S::x; // declares S::x, not a redefinition |
(自 C++17 起) |
- 類別名稱的宣告(透過前向宣告或在其他宣告中使用詳細類型說明符)
struct S; // declares, but does not define S class Y f(class T p); // declares, but does not define Y and T (and also f and p)
enum Color : int; // declares, but does not define Color |
(C++11 起) |
- 樣板參數的宣告
template<typename T> // declares, but does not define T
- 非定義之函式宣告中的參數宣告
int f(int x); // declares, but does not define f and x int f(int x) // defines f and x { return x + a; }
- typedef 宣告
typedef S S2; // declares, but does not define S2 (S may be incomplete)
using S2 = S; // declares, but does not define S2 (S may be incomplete) |
(C++11 起) |
using N::d; // declares, but does not define d
|
(自 C++17 起) |
|
(C++11 起) |
- 空宣告 (empty declaration)(不定義任何實體)
- using 指令(不定義任何實體)
extern template f<int, char>; // declares, but does not define f<int, char> |
(C++11 起) |
- 其宣告並非定義的顯式特化 (explicit specialization)
template<> struct A<int>; // declares, but does not define A<int>
asm 宣告不定義任何實體,但它被歸類為定義。
在必要時,編譯器可能會隱式定義預設建構函式、複製建構函式、移動建構函式、複製賦值運算子、移動賦值運算子以及解構函式。
若任何物件的定義導致產生不完整類型或抽象類別類型的物件,則該程式為格式錯誤 (ill-formed)。
目錄 |
[編輯] 單一定義規則
在任何單一轉譯單元中,對於任何變數、函式、類別類型、列舉類型、概念 (concept)(C++20 起)或樣板,只允許有一個定義(這些項目可能有多次宣告,但只允許一個定義)。
在整個程式中(包含任何標準函式庫與使用者自定義函式庫),對於每個被 odr-使用 (odr-used)(見下文)的非 inline 函式或變數,必須且僅能有一個定義。編譯器不需要診斷此違反行為,但違反此規則的程式行為是未定義的。
對於 inline 函式或 inline 變數(C++17 起),在每個使用到它的轉譯單元中,都必須有一個定義。
對於類別,在任何需要該類別為完整 (complete) 的使用處,都必須有該類別的定義。
在一個程式中,以下每一項可以有多個定義:類別類型、列舉類型、inline 函式、inline 變數(C++17 起)、樣板化實體(樣板或樣板的成員,但不包括完整的樣板特化),只要滿足以下所有條件:
- 每個定義出現在不同的轉譯單元中。
| (自 C++20 起) |
- 每個定義由相同的記號 (tokens)序列組成(通常出現在同一個標頭檔中)。
- 在每個定義內部進行名稱查找(在多載解析後)會找到相同的實體,除非:
- 具有內部連結或無連結的常數可以指向不同的物件,只要它們沒有被 odr-使用,且在每個定義中具有相同的值。
|
(C++11 起) |
- 多載運算子(包含轉換、配置與解配置函式)在每個定義中都參照相同的函式(除非是參照在定義內部所定義的函式)。
- 對應的實體在每個定義中具有相同的語言連結(例如,include 檔案不在 extern "C" 區塊內)。
- 若
const物件在任何定義中是常數初始化 (constant-initialized) 的,則它在每個定義中均為常數初始化的。 - 上述規則適用於每個定義中所使用的所有預設引數。
- 若定義是關於一個具有隱式宣告建構函式的類別,則在其被 odr-使用的每個轉譯單元中,必須對基礎類別與成員呼叫相同的建構函式。
|
(自 C++20 起) |
- 若定義是關於一個樣板,則所有這些要求同時適用於定義點的名稱,以及實例化點的相依名稱。
若滿足所有這些要求,則程式的行為如同整個程式中只有一個定義。否則,程式格式錯誤,無需診斷。
註:在 C 語言中,沒有針對類型的程式級 ODR,即使在不同轉譯單元中對相同變數的 extern 宣告,只要它們相容 (compatible),類型就可以不同。在 C++ 中,相同類型的宣告所使用的原始碼記號必須如同上述描述般相同:如果一個 .cpp 檔案定義了 struct S { int x; }; 而另一個 .cpp 檔案定義了 struct S { int y; };,將它們連結在一起的程式行為是未定義的。這通常透過匿名命名空間解決。
[編輯] 命名實體
若運算式是一個標識符表達式且指涉該變數,則該運算式即 命名 (names) 了該變數。
在下列情況中,運算式或轉換 命名 了函式:
- 若一個函式的名稱以運算式或轉換形式出現(包含具名函式、多載運算子、使用者自定義轉換、operator new 的使用者自定義配置形式、非預設初始化),且該函式由多載解析所選中,則該運算式命名了該函式,但未限定的純虛擬成員函式或純虛擬函式的成員指標除外。
- 類別的配置或解配置函式,由運算式中出現的 new 運算式所命名。
- 類別的解配置函式由運算式中出現的 delete 運算式所命名。
- 即使發生了複製省略 (copy elision),被選中用於複製或移動物件的建構函式仍被視為由該運算式或轉換所命名。在某些上下文中,使用 prvalue 不會複製或移動物件,請參閱強制省略。(C++17 起)
潛在求值的運算式或轉換,若命名了函式,即 odr-使用了該函式。
|
命名 constexpr 函式的潛在常數求值運算式或轉換,會使該函式變為需要常數求值,這會觸發預設函式的定義或函式樣板特化的實例化,即使該運算式未求值亦然。 |
(C++11 起) |
[編輯] 潛在結果
運算式 E 的 潛在結果 (potential results) 集合,是一個包含在 E 內的標識符表達式集合(可能為空),組合方式如下:
- 若 E 是標識符表達式,則運算式 E 是其唯一的潛在結果。
- 若 E 是下標運算式 (E1[E2]),其中一個運算元是陣列,則該運算元的潛在結果會包含在集合中。
- 若 E 是 E1.E2 或 E1.template E2 形式且命名了非靜態資料成員的類別成員存取運算式,則 E1 的潛在結果會包含在集合中。
- 若 E 是命名了靜態資料成員的類別成員存取運算式,則指涉該資料成員的標識符表達式會包含在集合中。
- 若 E 是 E1.*E2 或 E1.*template E2 形式且第二個運算元為常數運算式的成員指標存取運算式,則 E1 的潛在結果會包含在集合中。
- 若 E 是括號中的運算式 ((E1)),則 E1 的潛在結果會包含在集合中。
- 若 E 是 glvalue 條件運算式 (E1 ? E2 : E3,其中 E2 與 E3 為 glvalues),則 E2 與 E3 的潛在結果之聯集會包含在集合中。
- 若 E 是逗號運算式 (E1, E2),則 E2 的潛在結果會包含在集合中。
- 否則,該集合為空。
[編輯] ODR-使用(非正式定義)
若物件的值被讀取(除非是編譯期常數)或寫入、取址,或有參照綁定至該物件,則該物件被 odr-使用。
若參照被使用且其所指對象在編譯期未知,則該參照被 odr-使用。
若函式被呼叫或被取址,則該函式被 odr-使用。
若一個實體被 odr-使用,則其定義必須存在於程式中的某處;違反此規則通常會導致連結期錯誤。
struct S { static const int x = 0; // static data member // a definition outside of class is required if it is odr-used }; const int& f(const int& r); int n = b ? (1, S::x) // S::x is not odr-used here : f(S::x); // S::x is odr-used here: a definition is required
[編輯] ODR-使用(正式定義)
變數 x 若由出現於 P 點的潛在求值運算式 expr 所命名,除非滿足下列任一條件,否則該 expr 會 odr-使用 x:
- x 為在
P點可用於常數運算式的參照。 - x 非參照且 (C++26 前)expr 為運算式 E 的潛在結果集合中的一個元素,且滿足下列任一條件:
- E 為棄值運算式 (discarded-value expression),且未對其套用左值轉右值轉換。
- x 為在
P點可用於常數運算式、且不具有 mutable 子物件的非 volatile(C++26 起)物件,且滿足下列任一條件:
| (C++26 起) |
- E 為非 volatile 限定的非類別類型,且對其套用了左值轉右值轉換。
struct S { static const int x = 1; }; // applying lvalue-to-rvalue conversion // to S::x yields a constant expression int f() { S::x; // discarded-value expression does not odr-use S::x return S::x; // expression where lvalue-to-rvalue conversion // applies does not odr-use S::x }
若 this 以潛在求值運算式出現(包含非靜態成員函式呼叫運算式中的隱式 this),則 *this 被 odr-使用。
|
結構化綁定 (structured binding) 若以潛在求值運算式出現,即被 odr-使用。 |
(自 C++17 起) |
在下列情況中,函式被 odr-使用:
- 若函式由潛在求值運算式或轉換所命名(見上文),則該函式被 odr-使用。
- 若虛擬成員函式不是純虛擬成員函式,則它被 odr-使用(建構 vtable 時需要虛擬成員函式的位址)。
- 類別的非配置 (non-placement) 配置或解配置函式,由該類別的建構函式定義所 odr-使用。
- 類別的非配置解配置函式由該類別的解構函式定義所 odr-使用,或由虛擬解構函式的定義點所進行的查找選中時被 odr-使用。
- 另一類別
U的成員或基礎類別之類別T中的賦值運算子,由U的隱式定義之複製賦值或移動賦值函式所 odr-使用。 - 類別的建構函式(包含預設建構函式)由選中它的初始化所 odr-使用。
- 若類別的解構函式被潛在呼叫,則它被 odr-使用。
| 本節尚不完整 原因:odr-使用會造成影響的所有情況列表 |
[編輯] 缺陷報告
下列更改行為的缺陷報告追溯應用於之前的 C++ 標準。
| DR | 應用於 | 出版時的行為 | 正確的行為 |
|---|---|---|---|
| CWG 261 | C++98 | 多型類別的解配置函式 即使程式中沒有相關的 new 或 delete 運算式,也可能被 odr-使用 |
補充了 odr-使用的情況以涵蓋 建構函式與解構函式 |
| CWG 678 | C++98 | 實體可能擁有 不同語言連結的定義 |
在這種情況下,行為是 未定義的 |
| CWG 1472 | C++98 | 滿足常數運算式需求且 出現在常數運算式中的參照變數,即使 立即套用了左值轉右值轉換,也會被 odr-使用 |
它們不是 在此情況下被 odr-使用 |
| CWG 1614 | C++98 | 取得純虛擬函式的位址會導致其被 odr-使用 | 該函式不被 odr-使用 |
| CWG 1741 | C++98 | 在潛在求值運算式中立即被左值轉右值 轉換的常數物件會被 odr-使用 |
它們不被 odr-使用 |
| CWG 1926 | C++98 | 陣列下標運算式不會傳遞潛在結果 | 它們會進行傳遞 |
| CWG 2242 | C++98 | 不清楚只在定義的一部分中進行 常數初始化的 const 物件是否違反 ODR |
未違反 ODR;在此情況下,該物件 為常數初始化的 |
| CWG 2300 | C++11 | 不同轉譯單元中的 Lambda 運算式 永遠無法擁有相同的閉包類型 |
閉包類型可以在單一定義規則下 相同 |
| CWG 2353 | C++98 | 靜態資料成員不是存取它的 成員存取運算式的潛在結果 |
現在是了 |
| CWG 2433 | C++14 | 變數樣板不能在程式中擁有 多個定義 |
現在可以 |
[編輯] 參考資料
- C++23 標準 (ISO/IEC 14882:2024)
- 6.3 單一定義規則 [basic.def.odr]
- C++20 標準 (ISO/IEC 14882:2020)
- 6.3 單一定義規則 [basic.def.odr]
- C++17 標準 (ISO/IEC 14882:2017)
- 6.2 單一定義規則 [basic.def.odr]
- C++14 標準 (ISO/IEC 14882:2014)
- 3.2 單一定義規則 [basic.def.odr]
- C++11 標準 (ISO/IEC 14882:2011)
- 3.2 單一定義規則 [basic.def.odr]
- C++03 標準 (ISO/IEC 14882:2003)
- 3.2 單一定義規則 [basic.def.odr]
- C++98 標準 (ISO/IEC 14882:1998)
- 3.2 單一定義規則 [basic.def.odr]