名稱空間
變體
操作

定義和 ODR (One Definition Rule)

來自 cppreference.com
< cpp‎ | 語言
 
 
C++ 語言
 
 

定義宣告,它們完整地定義了宣告所引入的實體。每個宣告都是一個定義,除了以下情況:

  • 沒有函式體的函式宣告
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
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
  • (已棄用) 名稱空間作用域內使用 constexpr 說明符在類內部定義的靜態資料成員的宣告
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 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 起)
extern template
f<int, char>; // declares, but does not define f<int, char>
(C++11 起)
template<>
struct A<int>; // declares, but does not define A<int>

asm 宣告不定義任何實體,但被歸類為定義。

在必要時,編譯器可以隱式定義預設建構函式複製建構函式移動建構函式複製賦值運算子移動賦值運算子解構函式

如果任何物件的定義導致不完整型別抽象類型別的物件,則程式格式錯誤。

目錄

[編輯] 一個定義規則

在任何一個翻譯單元中,任何變數、函式、類型別、列舉型別概念(C++20 起)或模板只允許一個定義(其中一些可能具有多個宣告,但只允許一個定義)。

整個程式(包括任何標準庫和使用者定義庫)中,每個非inline函式或變數,如果被odr-used(見下文),都必須且只能出現一個定義。編譯器不要求診斷此違規行為,但違反此規則的程式行為是未定義的。

對於 inline 函式或 inline 變數(C++17 起),在每個其被odr-used的翻譯單元中都需要一個定義。

對於類,在任何以要求其為完整的方式使用該類的地方,都需要一個定義。

程式中可以有以下各項的多個定義:類型別、列舉型別、inline 函式、inline 變數(C++17 起)模板實體(模板或模板的成員,但不是完整的模板特化),只要滿足以下所有條件:

  • 每個定義出現在不同的翻譯單元中。
(C++20 起)
  • 每個定義由相同的標記序列組成(通常出現在相同的標頭檔案中)。
  • 從每個定義內部進行的名稱查詢會找到相同的實體(在過載決議之後),除了:
  • 具有內部或無連結的常量可以引用不同的物件,只要它們未被 odr-used 並在每個定義中具有相同的值。
  • 不在預設引數或預設模板引數(C++20 起)中的Lambda 表示式透過用於定義它們的標記序列唯一標識。
(C++11 起)
  • 過載運算子,包括轉換、分配和解分配函式,從每個定義引用相同的函式(除非引用在定義內部定義的函式)。
  • 每個定義中相應的實體具有相同的語言連結(例如,包含檔案不在 extern "C" 塊內)。
  • 如果 const 物件在任何定義中被常量初始化,則它在每個定義中都被常量初始化。
  • 上述規則適用於每個定義中使用的每個預設引數。
  • 如果定義是帶有隱式宣告建構函式的類,則在每個其被 odr-used 的翻譯單元中,必須為基類和成員呼叫相同的建構函式。
  • 如果定義是帶有預設三路比較的類,則在每個其被 odr-used 的翻譯單元中,必須為基類和成員呼叫相同的比較運算子。
(C++20 起)
  • 如果定義是模板,則所有這些要求適用於定義點的名稱和例項化點的依賴名稱。

如果所有這些要求都滿足,則程式行為就如同整個程式中只有一個定義。否則,程式格式錯誤,無需診斷。

注意:在 C 中,型別沒有程式範圍的 ODR,即使不同翻譯單元中同一變數的 extern 宣告也可以具有不同的型別,只要它們相容。在 C++ 中,如上所述,用於同一型別宣告的原始碼標記必須相同:如果一個 .cpp 檔案定義 struct S { int x; };,而另一個 .cpp 檔案定義 struct S { int y; };,則將它們連結在一起的程式行為是未定義的。這通常透過匿名名稱空間解決。

[編輯] 命名實體

如果表示式是表示變數的識別符號表示式,則變數被表示式命名

在以下情況下,函式被表示式或轉換命名

  • 如果函式名作為表示式或轉換出現(包括命名函式、過載運算子、使用者定義轉換operator new 的使用者定義放置形式、非預設初始化),並且它被過載決議選中,則該函式被該表示式命名,除非它是非限定純虛成員函式或指向純虛擬函式的成員指標。
  • 類的一個分配解分配函式被表示式中出現的new 表示式命名。
  • 類的一個解分配函式被表示式中出現的delete 表示式命名。
  • 即使發生複製消除,為複製或移動物件而選擇的建構函式也被表示式或轉換命名。在某些上下文中使用 prvalue 不會複製或移動物件,參見強制消除(C++17 起)

潛在求值表示式或轉換在命名函式時會 odr-use 它。

命名 constexpr 函式的潛在常量求值表示式或轉換使其在常量求值中需要,這會觸發預設函式的定義或函式模板特化的例項化,即使表示式未求值。

(C++11 起)

[編輯] 潛在結果

表示式 E潛在結果集合是(可能為空)出現在 E 中的識別符號表示式集合,結合方式如下:

  • 如果 E識別符號表示式,則表示式 E 是其唯一的潛在結果。
  • 如果 E 是下標表達式(E1[E2]),其中一個運算元是陣列,則該運算元的潛在結果包含在集合中。
  • 如果 E 是形式為 E1.E2E1.template E2 的類成員訪問表示式,命名非靜態資料成員,則 E1 的潛在結果包含在集合中。
  • 如果 E 是命名靜態資料成員的類成員訪問表示式,則指定該資料成員的識別符號表示式包含在集合中。
  • 如果 E 是形式為 E1.*E2E1.*template E2 的指向成員的指標訪問表示式,其第二個運算元是常量表達式,則 E1 的潛在結果包含在集合中。
  • 如果 E 是括號中的表示式((E1)),則 E1 的潛在結果包含在集合中。
  • 如果 E 是 glvalue 條件表示式(E1 ? E2 : E3,其中 E2E3 是 glvalue),則 E2E3 的潛在結果的並集都包含在集合中。
  • 如果 E 是逗號表示式(E1, E2),則 E2 的潛在結果在潛在結果集合中。
  • 否則,集合為空。

[編輯] ODR-use(非正式定義)

如果物件的的值被讀取(除非它是編譯時常量)或寫入,其地址被獲取,或一個引用繫結到它,則該物件被 odr-used。

如果引用被使用且其指向的實體在編譯時未知,則該引用被 odr-used。

如果函式被呼叫或其地址被獲取,則該函式被 odr-used。

如果一個實體被 odr-used,則其定義必須存在於程式中的某個位置;違反此規則通常會導致連結時錯誤。

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-use(正式定義)

變數 x 被在點 P 出現的潛在求值表示式 expr 命名,並且被 expr odr-used,除非滿足以下任何條件:

  • x 是在 P可在常量表達式中使用的引用。
  • x 不是引用,並且(直至 C++26)expr 是表示式 E 的潛在結果集的一個元素,並且滿足以下任何條件:
    • E 是一個被丟棄值的表示式,並且沒有對其應用左值到右值轉換。
    • x 是一個在 P 處可在常量表達式中使用的非 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-used。

如果結構化繫結作為潛在求值表示式出現,則它被 odr-used。

(C++17 起)

函式在以下情況下被 odr-used:

  • 如果函式被潛在求值表示式或轉換命名(見下文),則該函式被 odr-used。
  • 如果虛成員函式不是純虛成員函式,則它被 odr-used(虛成員函式的地址是構建虛擬函式表所必需的)。
  • 類的一個非放置分配或解分配函式被該類的建構函式的定義 odr-used。
  • 類的一個非放置解分配函式被該類的解構函式的定義 odr-used,或者在虛解構函式的定義點透過查詢被選中而 odr-used。
  • T 中作為另一個類 U 的成員或基類的賦值運算子被 U 的隱式定義複製賦值或移動賦值函式 odr-used。
  • 為類選擇的建構函式(包括預設建構函式)被選擇它的初始化 odr-used。
  • 如果類的解構函式可能被呼叫,則它被 odr-used。

[編輯] 缺陷報告

下列更改行為的缺陷報告追溯地應用於以前出版的 C++ 標準。

缺陷報告 應用於 釋出時的行為 正確的行為
CWG 261 C++98 多型類的解分配函式
即使程式中沒有相關的新建或刪除表示式,也可能被 odr-used
在程式中沒有相關新建或刪除表示式的情況下,多型類的解分配函式也可能被 odr-used。
補充了
odr-use 案例以涵蓋
建構函式和解構函式
CWG 678 C++98 一個實體可以有不同語言連結的定義
一個實體可以有
在這種情況下,行為是
未定義的
具有不同語言連結的定義 C++98 CWG 1472
滿足常量表達式要求的引用變數即使立即應用了左值到右值轉換也曾被 odr-used
即使立即應用了左值到右值轉換,滿足常量表達式要求的引用變數也會被 odr-used。
它們沒有
在這種情況下被 odr-used
CWG 1614 C++98 獲取純虛擬函式的地址會 odr-use 它 該函式未被 odr-used
CWG 1741 C++98 在潛在求值表示式中立即進行左值到右值轉換的常量物件曾經被 odr-used
在潛在求值表示式中,立即進行左值到右值轉換的常量物件曾被 odr-used。
它們未被 odr-used
CWG 1926 C++98 陣列下標表達式未傳播潛在結果 它們傳播
CWG 2242 C++98 不清楚只在其部分定義中常量初始化的 const 物件是否違反 ODR
不清楚只在部分定義中常量初始化的 const 物件是否違反 ODR
ODR 未被違反;物件在此情況下被常量初始化
在此情況下被常量初始化
CWG 2300 C++11 不同翻譯單元中的 lambda 表示式
永遠不能具有相同的閉包型別
閉包型別在“一個定義規則”下可以是相同的
閉包型別在
一個定義規則下可以是相同的 C++98 CWG 2353
靜態資料成員不是訪問它的成員訪問表示式的潛在結果
它是
靜態資料成員不是訪問它的成員訪問表示式的潛在結果。 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]