名前空間
変種
操作

定義と ODR

提供: cppreference.com
< cpp‎ | language
 
 
C++言語
一般的なトピック
フロー制御
条件付き実行文
繰り返し文 (ループ)
ジャンプ文
関数
関数宣言
ラムダ関数宣言
inline 指定子
例外指定 (C++20未満)
noexcept 指定子 (C++11)
例外
名前空間
指定子
decltype (C++11)
auto (C++11)
alignas (C++11)
記憶域期間指定子
初期化
代替表現
リテラル
ブーリアン - 整数 - 浮動小数点
文字 - 文字列 - nullptr (C++11)
ユーザ定義 (C++11)
ユーティリティ
属性 (C++11)
typedef 宣言
型エイリアス宣言 (C++11)
キャスト
暗黙の変換 - 明示的な変換
static_cast - dynamic_cast
const_cast - reinterpret_cast
メモリ確保
クラス
クラス固有の関数特性
特別なメンバ関数
テンプレート
その他
 
 

定義は、その宣言によって導入されるエンティティを完全に定義する宣言です。 以下を除くすべての宣言は定義です。

  • 関数本体を持たない関数宣言。
int f(int); // f を宣言しますが、定義しません。
extern const int a; // a を宣言しますが、定義しません。
extern const int b = 1; // b を定義します。
struct S {
    int n;               // S::n を定義します。
    static int i;        // S::i を宣言しますが、定義しません。
    inline static int x; // S::x を定義します。
};                       // S を定義します。
int S::i;                // S::i を定義します。
  • (非推奨) constexpr 指定子を持つクラス内で定義された静的データメンバの名前空間スコープの宣言。
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 S S2; // S2 を宣言しますが、定義しません (S は不完全でも構いません)。
using S2 = S; // S2 を宣言しますが、定義しません (S は不完全でも構いません)。
(C++11以上)
using N::d; // d を宣言しますが、定義しません。
  • 推定ガイドの宣言 (いかなるエンティティも定義しません)。
(C++17以上)
  • static_assert 宣言 (いかなるエンティティも定義しません)。
  • 属性宣言 (いかなるエンティティも定義しません)。
(C++11以上)
  • 空宣言 (いかなるエンティティも定義しません)。
  • using 指令 (いかなるエンティティも定義しません)。
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++17以上)、クラステンプレート、非静的関数テンプレート、クラステンプレートの静的データメンバ、クラステンプレートのメンバ関数、テンプレートの部分特殊化コンセプト (C++20以上)の定義は、それぞれの定義が異なる翻訳単位内に現れる限り、プログラム内に複数存在することができます。

  • それぞれの定義が同じトークンの並びから構成される (一般的には、同じヘッダファイル内に現れます)。
  • それぞれの定義内で行われる名前探索が (オーバーロード解決後に) 同じエンティティを発見する。 ただし、内部リンケージを持つまたはリンケージを持たない定数は、それらが ODR 使用されず、すべての定義が同じ値を持つ限り、異なるオブジェクトを参照しても構いません。
  • オーバーロード演算子 (変換、確保、解放関数を含みます) がそれぞれの定義内の同じ関数を参照する (その定義内で定義されたものを参照する場合は除きます)。
  • 言語リンケージが同じである (例えば、インクルードファイルが extern "C" ブロックの内側でない)。
  • 上記3つのルールは、それぞれの定義内で使用されるすべてのデフォルト引数に適用されます。
  • その定義が、事前条件 ([[expects:]]) を持つ関数を呼ぶ、またはアサーション ([[assert:]]) を含む関数である、または契約条件 ([[expects:]] または [[ensures:]]) を持つ場合、どの条件下ですべての定義が同じビルドレベルおよび違反継続モードを用いて翻訳されなければならないかは処理系定義です。
(C++20以上)
  • 定義が暗黙に宣言されたコンストラクタを持つクラスの場合、それが ODR 使用されるすべての翻訳単位は、基底およびメンバに対する同じコンストラクタを呼ばなければなりません。
  • 定義がテンプレートの場合、これらのすべての要件が定義時点の名前と実体化時点の依存名の両方に適用されます。

これらのすべての要件が満たされるならば、プログラム全体で定義が一度だけであるかのように動作します。 そうでなければ、動作は未定義です。

ノート: 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 使用されます。 定義が要求されます。

形式的には、

1) 潜在的に評価される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 使用しません。
}
2) *this は、 this潜在的に評価される式 (非静的メンバ関数呼び出し式の暗黙の this を含みます) として現れた場合、 ODR 使用されます。
3) 構造化束縛は、それが潜在的に評価される式として現れた場合、 ODR 使用されます。
(C++17以上)

上記の定義において、潜在的に評価されるとは、その式が sizeof の被演算子などの未評価被演算子 (またはその部分式) でないことを意味し、式 e潜在的な結果の集合は、 e 内に現れる識別子式の集合 (空であることもあります) を以下のように組み合わせたものです。

  • e識別子式である場合、式 e はその唯一の潜在的な結果です。
  • e が配列添字式 (e1[e2]) であり、その被演算子の一方が配列である場合、その被演算子の潜在的な結果が集合に含まれます。
(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) である場合、 e2e3 の潜在的な結果の和が両方とも集合に含まれます。
  • 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 使用されません。
}
4) 関数は、以下の場合、 ODR 使用されます。
  • 名前が潜在的に評価される式として現れる関数 (名前付きの関数、オーバーロード演算子、ユーザ定義変換、 operator new のユーザ定義配置形式、非デフォルト初期化を含みます) は、それがオーバーロード解決によって選択された場合、 ODR 使用されます。 ただし、それが無修飾の純粋仮想メンバ関数または純粋仮想関数へのメンバポインタ (C++17以上)であるときは除きます。
  • 仮想メンバ関数は、それが純粋仮想メンバ関数でない場合、 ODR 使用されます (vtable を構築するために仮想メンバ関数のアドレスが要求されます)。
  • クラスに対する確保関数または解放関数は、潜在的に評価される式に現れる new 式によって、 ODR 使用されます。
  • クラスに対する解放関数は、潜在的に評価される式に現れる delete 式によって ODR 使用されます。
  • クラスに対する非配置確保または解放関数は、そのクラスに対するコンストラクタの定義によって ODR 使用されます。
  • クラスに対する非配置解放関数は、そのクラスのデストラクタの定義によって、または仮想デストラクタの定義の地点における名前探索によって選択されることによって、 ODR 使用されます。
  • 別のクラス U のメンバまたは基底であるクラス T の代入演算子は、 U の暗黙に定義されたコピー代入演算子またはムーブ代入演算子によって ODR 使用されます。
  • クラスに対するコンストラクタ (デフォルトコンストラクタを含みます) は、それを選択する初期化によって ODR 使用されます。
  • クラスに対するデストラクタは、それが潜在的に呼び出される場合、 ODR 使用されます。

すべてのクラスにおいて、オブジェクトをコピーまたはムーブするために選択されるコンストラクタは、たとえコピー省略が行われる場合でも、 ODR 使用されます。

[編集] 参考文献

  • C++11 standard (ISO/IEC 14882:2011):
  • 3.2 One definition rule [basic.def.odr]