定義と ODR
定義は、その宣言によって導入されるエンティティを完全に定義する宣言です。 以下を除くすべての宣言は定義です。
- 関数本体を持たない関数宣言。
int f(int); // f を宣言しますが、定義しません。
extern const int a; // a を宣言しますが、定義しません。 extern const int b = 1; // b を定義します。
- クラス定義の内側のインラインでない (C++17以上)静的データメンバの宣言。
struct S { int n; // S::n を定義します。 static int i; // S::i を宣言しますが、定義しません。 inline static int x; // S::x を定義します。 }; // S を定義します。 int S::i; // S::i を定義します。
struct S { static constexpr int x = 42; // 暗黙のインライン。 S::x を定義します。 }; constexpr int S::x; // S::x を宣言します。 再定義ではありません。 |
(C++17以上) |
- クラス名の宣言 (前方宣言による、または別の宣言内の複雑型指定子の使用による)
struct S; // S を宣言しますが、定義しません。 class Y f(class T p); // Y と T (および f と p も) を宣言しますが、定義しません。
enum Color : int; // Color を宣言しますが、定義しません。 |
(C++11以上) |
- テンプレート引数の宣言。
template<typename T> // T を宣言しますが、定義しません。
- 定義でない関数宣言内の引数宣言。
int f(int x); // f と x を宣言しますが、定義しません。 int f(int x) { // f と x を定義します。 return x+a; }
- typedef 宣言。
typedef S S2; // S2 を宣言しますが、定義しません (S は不完全でも構いません)。
using S2 = S; // S2 を宣言しますが、定義しません (S は不完全でも構いません)。 |
(C++11以上) |
using N::d; // d を宣言しますが、定義しません。
|
(C++17以上) |
|
(C++11以上) |
extern template f<int, char>; // f<int, char> を宣言しますが、定義しません。 |
(C++11以上) |
- その宣言が定義でない明示的特殊化。
template<> struct A<int>; // A<int> を宣言しますが、定義しません。
asm 宣言は、いかなるエンティティも定義しませんが、定義に分類されます。
コンパイラは、必要な場面では、デフォルトコンストラクタ、コピーコンストラクタ、ムーブコンストラクタ、コピー代入演算子、ムーブ代入演算子、およびデストラクタを、暗黙に定義する場合があります。
何らかのオブジェクトの定義の結果が、不完全型または抽象クラス型のオブジェクトである場合、プログラムは ill-formed です。
[編集] 単一定義規則 (ODR)
いずれの変数、関数、クラス型、列挙型、コンセプト (C++20以上)またはテンプレートの定義も、いずれかひとつの翻訳単位内で、一度のみ許されます (宣言は複数あっても構いませんが、定義は一度しか許されません)。
ODR 使用 (後述) される、インラインでないすべての関数または変数の定義が、プログラム全体で、一度だけ要求されます。 コンパイラはこの違反を診断することを要求されませんが、これに違反するプログラムの動作は未定義です。
インライン関数またはインライン変数 (C++17以上)の場合、定義は、それが ODR 使用されるすべての翻訳単位で要求されます。
クラスの定義は、そのクラスが完全であることを要求する方法で使用されるあらゆる翻訳単位で、一度だけ現れることが要求されます。
以下のすべてが真である限り、クラス型、列挙型、インライン関数、テンプレート化されたエンティティ (テンプレートまたはテンプレートのメンバ、ただしテンプレートの完全特殊化は含みません) の定義は、プログラム内に複数存在することができます。
- それぞれの定義が異なる翻訳単位に現れる。
|
(C++20以上) |
- それぞれの定義が同じトークンの並びから構成される (一般的には、同じヘッダファイル内に現れる)。
- それぞれの定義内で行われる名前探索が (オーバーロード解決後に) 同じエンティティを発見する。 ただし、
- 内部リンケージを持つまたはリンケージを持たない定数は、それらが ODR 使用されず、すべての定義が同じ値を持つ限り、異なるオブジェクトを参照しても構いません。
|
(C++11以上) |
- オーバーロード演算子 (変換、確保、解放関数を含みます) がそれぞれの定義内の同じ関数を参照する (その定義内で定義されたものを参照する場合は除きます)。
- 言語リンケージが同じである (例えば、インクルードファイルが extern "C" ブロックの内側でない)。
- 上記3つのルールは、それぞれの定義内で使用されるすべてのデフォルト引数に適用されます。
- 定義が暗黙に宣言されたコンストラクタを持つクラスの場合、それが ODR 使用されるすべての翻訳単位は、基底およびメンバに対する同じコンストラクタを呼ばなければなりません。
|
(C++20以上) |
- 定義がテンプレートの場合、これらのすべての要件が定義時点の名前と実体化時点の依存名の両方に適用されます。
これらのすべての要件が満たされるならば、プログラム全体で定義が一度だけであるかのように動作します。 そうでなければ、プログラムは ill-formed です (診断は要求されません)。
ノート: C では、型に対するプログラムワイドな ODR はありません。 さらに、互換である限り、同じ変数の extern 宣言が、異なる翻訳単位で異なる型を持つことさえできます。 C++ では、上で述べた通り、同じ型の宣言で使用されるソースコードトークンは同じでなければなりません。 ある .cpp ファイルで struct S { int x; };
を定義し、別の .cpp ファイルで struct S { int y; };
を定義した場合、それらを一緒にリンクするプログラムの動作は未定義です。 これは通常、無名名前空間を用いて解決されます。
[編集] ODR 使用
非形式的には、オブジェクトは、値が読み込まれる (コンパイル時定数である場合は除きます)、書き込まれる、アドレスが取られる、または参照がそれに束縛される場合に、 ODR 使用されます。 参照は、それが使用され、その参照先がコンパイル時に既知でない場合に、 ODR 使用されます。 関数は、それに対する関数呼び出しが行われるか、アドレスが取られる場合に、 ODR 使用されます。 オブジェクト、参照または関数が ODR 使用される場合は、その定義がプログラム内のどこかに存在しなければなりません。 その違反は、通常、リンク時のエラーです。
struct S { static const int x = 0; // 静的データメンバ。 // ODR 使用される場合は、クラスの外側の定義が要求されます。 }; const int& f(const int& r); int n = b ? (1, S::x) // S::x はここでは ODR 使用されません。 : f(S::x); // S::x はここでは ODR 使用されます。 定義が要求されます。
形式的には、
ex
内の変数 x
は、以下の両方が真でない限り、 ODR 使用されます。
-
x
への左辺値から右辺値への変換が非トリビアルな関数を呼ばない定数式を生成する。 -
x
がオブジェクトでない (つまり、x
が参照である)、または、x
がオブジェクトの場合は、より大きな式e
の潜在的な結果のひとつである。 ただし、より大きな式は、値を破棄する式であるか、それに適用される左辺値から右辺値への変換を持つか、のいずれかです。
-
struct S { static const int x = 1; }; // S::x への左辺値から右辺値への変換は定数式を生成します。 int f() { S::x; // 値を破棄する式は S::x を ODR 使用しません。 return S::x; // 左辺値から右辺値への変換が適用される式は S::x を ODR 使用しません。 }
3) 構造化束縛は、それが潜在的に評価される式として現れた場合、 ODR 使用されます。
|
(C++17以上) |
上記の定義において、潜在的に評価されるとは、その式が sizeof の被演算子などの未評価被演算子 (またはその部分式) でないことを意味し、式 e
の潜在的な結果の集合は、 e
内に現れる識別子式の集合 (空であることもあります) を以下のように組み合わせたものです。
-
e
が識別子式である場合、式e
はその唯一の潜在的な結果です。
|
(C++17以上) |
-
e
がクラスメンバアクセス式 (e1.e2 または e1->e2) である場合、そのオブジェクト式 e1 の潜在的な結果が集合に含まれます。 -
e
がメンバポインタアクセス式 (e1.*e2 または e1->*e2) であり、その第2被演算子が定数式である場合、そのオブジェクト式 e1 の潜在的な結果が集合に含まれます。 -
e
が丸括弧で囲まれた式 ((e1)) である場合、e1
の潜在的な結果が集合に含まれます。 -
e
が glvalue の条件式 (e2 と e3 が glvalue であるような e1?e2:e3) である場合、e2
とe3
の潜在的な結果の和が両方とも集合に含まれます。 -
e
がコンマ式 (e1,e2) である場合、e2
の潜在的な結果が集合に含まれます。 - そうでなければ、集合は空です。
struct S { static const int a = 1; static const int b = 2; }; int f(bool x) { return x ? S::a : S::b; // x は部分式「x」 (? の左) の一部であり、 // これは左辺値から右辺値への変換を適用しますが、 // x へのその変換の適用は定数式を生成しないため、 x は ODR 使用されます。 // S::a と S::b は左辺値であり、 glvalue の条件式の結果に // 「潜在的な結果」として持ち越されます。 // この結果は、戻り値をコピー初期化するために要求される左辺値から右辺値への // 変換の対象であり、そのため S::a と S::b は ODR 使用されません。 }
- 名前が潜在的に評価される式として現れる関数 (名前付きの関数、オーバーロード演算子、ユーザ定義変換、 operator new のユーザ定義配置形式、非デフォルト初期化を含みます) は、それがオーバーロード解決によって選択された場合、 ODR 使用されます。 ただし、それが無修飾の純粋仮想メンバ関数または純粋仮想関数へのメンバポインタ (C++17以上)であるときは除きます。
- 仮想メンバ関数は、それが純粋仮想メンバ関数でない場合、 ODR 使用されます (vtable を構築するために仮想メンバ関数のアドレスが要求されます)。
- クラスに対する確保関数または解放関数は、潜在的に評価される式に現れる new 式によって、 ODR 使用されます。
- クラスに対する解放関数は、潜在的に評価される式に現れる delete 式によって ODR 使用されます。
- クラスに対する非配置確保または解放関数は、そのクラスに対するコンストラクタの定義によって ODR 使用されます。
- クラスに対する非配置解放関数は、そのクラスのデストラクタの定義によって、または仮想デストラクタの定義の地点における名前探索によって選択されることによって、 ODR 使用されます。
- 別のクラス
U
のメンバまたは基底であるクラスT
の代入演算子は、U
の暗黙に定義されたコピー代入演算子またはムーブ代入演算子によって ODR 使用されます。 - クラスに対するコンストラクタ (デフォルトコンストラクタを含みます) は、それを選択する初期化によって ODR 使用されます。
- クラスに対するデストラクタは、それが潜在的に呼び出される場合、 ODR 使用されます。
すべてのクラスにおいて、オブジェクトをコピーまたはムーブするために選択されるコンストラクタは、たとえコピー省略が行われる場合でも、 ODR 使用されます。
This section is incomplete Reason: list of all situations where odr-use makes a difference |
[編集] 参考文献
- C++11 standard (ISO/IEC 14882:2011):
- 3.2 One definition rule [basic.def.odr]