Definitionen und ODR (One Definition Rule)
Definitionen sind Deklarationen, die das durch die Deklaration eingeführte Entität vollständig definieren. Jede Deklaration ist eine Definition, außer den folgenden:
- Eine Funktionsdeklaration ohne Funktionsrumpf
int f(int); // declares, but does not define f
- Jede Deklaration mit einem extern Speicherklassenspezifizierer oder mit einem Sprachverknüpfungsspezifizierer (wie z.B. extern "C") ohne Initialisierer
extern const int a; // declares, but does not define a extern const int b = 1; // defines b
- Deklaration eines nicht-inline(seit C++17) statischen Datenmembers innerhalb einer Klassendefinition
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 |
(seit C++17) |
- Deklaration eines Klassennamens (mittels Vorwärtsdeklaration oder durch die Verwendung des elaborierten Typspezifizierers in einer anderen Deklaration)
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 |
(seit C++11) |
- Deklaration eines Template-Parameters
template<typename T> // declares, but does not define T
- Eine Parameterdeklaration in einer Funktionsdeklaration, die keine Definition ist
int f(int x); // declares, but does not define f and x int f(int x) // defines f and x { return x + a; }
- Eine typedef-Deklaration
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) |
(seit C++11) |
- Eine using-Deklaration
using N::d; // declares, but does not define d
|
(seit C++17) |
|
(seit C++11) |
- Eine leere Deklaration (definiert keine Entitäten)
- Eine using-Direktive (definiert keine Entitäten)
extern template f<int, char>; // declares, but does not define f<int, char> |
(seit C++11) |
- Eine explizite Spezialisierung, deren Deklaration keine Definition ist
template<> struct A<int>; // declares, but does not define A<int>
Eine asm-Deklaration definiert keine Entitäten, wird aber als Definition klassifiziert.
Wo nötig, kann der Compiler den Standardkonstruktor, den Kopierkonstruktor, den Verschiebekonstruktor, den Kopierungszuweisungsoperator, den Verschiebezweisungsoperator und den Destruktor implizit definieren.
Wenn die Definition eines beliebigen Objekts zu einem Objekt mit unvollständigem Typ oder einem abstrakten Klassentyp führt, ist das Programm schlecht geformt.
Inhalt |
[bearbeiten] One Definition Rule
Nur eine Definition einer beliebigen Variable, Funktion, eines Klassentyps, Aufzählungstyps, Konzept(seit C++20) oder eines Templates ist in jeder einzelnen Translation Unit erlaubt (einige davon dürfen mehrere Deklarationen haben, aber nur eine Definition ist erlaubt).
Eine und nur eine Definition jeder nicht-inline Funktion oder Variable, die odr-used (siehe unten) ist, muss im gesamten Programm (einschließlich aller Standard- und benutzerdefinierten Bibliotheken) erscheinen. Der Compiler ist nicht verpflichtet, diese Verletzung zu diagnostizieren, aber das Verhalten des Programms, das sie verletzt, ist undefiniert.
Für eine Inline-Funktion oder Inline-Variable(seit C++17) ist eine Definition in jeder Translation Unit erforderlich, in der sie odr-used ist.
Für eine Klasse ist eine Definition überall dort erforderlich, wo die Klasse auf eine Weise verwendet wird, die ihre Vollständigkeit erfordert.
Es kann mehr als eine Definition in einem Programm für jeden der folgenden Punkte geben: Klassentyp, Aufzählungstyp, Inline-Funktion, Inline-Variable(seit C++17), templated entity (Template oder Mitglied eines Templates, aber keine vollständige Template-Spezialisierung), solange alle folgenden Bedingungen erfüllt sind:
- Jede Definition erscheint in einer anderen Translation Unit.
|
(seit C++20) |
- Jede Definition besteht aus der gleichen Sequenz von Tokens (typischerweise erscheint sie in derselben Header-Datei).
- Die Namensauflösung aus jeder Definition findet dieselben Entitäten (nach Überladungsauflösung), außer dass
- Konstanten mit interner oder keiner Verknüpfung auf verschiedene Objekte verweisen dürfen, solange sie nicht odr-used sind und in jeder Definition die gleichen Werte haben.
|
(seit C++11) |
- Überladene Operatoren, einschließlich Konvertierungs-, Allokations- und Deallokationsfunktionen, verweisen von jeder Definition auf dieselbe Funktion (es sei denn, sie verweisen auf eine innerhalb der Definition definierte).
- Entsprechende Entitäten haben in jeder Definition die gleiche Sprachverknüpfung (z.B. ist die Include-Datei nicht innerhalb eines extern "C"-Blocks).
- Wenn ein
const-Objekt in einer der Definitionen konstant initialisiert wird, wird es in jeder Definition konstant initialisiert. - Die obigen Regeln gelten für jedes Standardargument, das in jeder Definition verwendet wird.
- Wenn die Definition eine Klasse mit einem implizit deklarierten Konstruktor ist, muss jede Translation Unit, in der sie odr-used ist, denselben Konstruktor für die Basisklasse und die Member aufrufen.
|
(seit C++20) |
- Wenn die Definition für ein Template ist, dann gelten all diese Anforderungen sowohl für Namen am Punkt der Definition als auch für abhängige Namen am Punkt der Instanziierung.
Wenn alle diese Anforderungen erfüllt sind, verhält sich das Programm so, als ob es nur eine Definition im gesamten Programm gäbe. Andernfalls ist das Programm schlecht geformt, keine Diagnose erforderlich.
Hinweis: In C gibt es keine programmweite ODR für Typen, und selbst extern Deklarationen derselben Variable in verschiedenen Translation Units können unterschiedliche Typen haben solange sie kompatibel sind. In C++ müssen die im Quellcode verwendeten Tokens für Deklarationen desselben Typs gleich sein, wie oben beschrieben: Wenn eine .cpp-Datei struct S { int x; }; definiert und die andere .cpp-Datei struct S { int y; }; definiert, ist das Verhalten des Programms, das sie verknüpft, undefiniert. Dies wird normalerweise mit anonymen Namespaces gelöst.
[bearbeiten] Benennung einer Entität
Eine Variable wird durch einen Ausdruck benannt, wenn der Ausdruck ein Bezeichnerausdruck ist, der sie bezeichnet.
Eine Funktion wird in folgenden Fällen durch einen Ausdruck oder eine Konvertierung benannt:
- Eine Funktion, deren Name als Ausdruck oder Konvertierung erscheint (einschließlich benannter Funktionen, überladener Operatoren, benutzerdefinierter Konvertierungen, benutzerdefinierter Placement-Formen von operator new, nicht-Standard-Initialisierung), wird durch diesen Ausdruck benannt, wenn sie von der Überladungsauflösung ausgewählt wird, es sei denn, es handelt sich um eine nicht qualifizierte reine virtuelle Memberfunktion oder einen Zeiger auf eine reine virtuelle Funktion.
- Eine Allokationsfunktion oder Deallokationsfunktion für eine Klasse wird durch einen new-Ausdruck benannt, der in einem Ausdruck erscheint.
- Eine Deallokationsfunktion für eine Klasse wird durch einen delete-Ausdruck benannt, der in einem Ausdruck erscheint.
- Ein Konstruktor, der zum Kopieren oder Verschieben eines Objekts ausgewählt wird, gilt als durch den Ausdruck oder die Konvertierung benannt, auch wenn eine Kopierelision stattfindet. Die Verwendung eines prvalue in einigen Kontexten kopiert oder verschiebt ein Objekt nicht, siehe obligatorische Elision.(seit C++17)
Ein potenziell ausgewerteter Ausdruck oder eine Konvertierung odr-used eine Funktion, wenn er sie benennt.
|
Ein potenziell konstant ausgewerteter Ausdruck oder eine Konvertierung, die eine constexpr-Funktion benennt, macht sie für die konstante Auswertung erforderlich, was die Definition einer standardmäßig definierten Funktion oder die Instanziierung einer Funktions-Template-Spezialisierung auslöst, selbst wenn der Ausdruck nicht ausgewertet wird. |
(seit C++11) |
[bearbeiten] Potenzielle Ergebnisse
Die Menge der potenziellen Ergebnisse eines Ausdrucks E ist eine (möglicherweise leere) Menge von Bezeichnerausdrücken, die innerhalb von E vorkommen und wie folgt kombiniert werden:
- Wenn E ein Bezeichnerausdruck ist, ist der Ausdruck E sein einziges potenzielles Ergebnis.
- Wenn E ein Subskriptausdruck (E1[E2]) ist, bei dem einer der Operanden ein Array ist, werden die potenziellen Ergebnisse dieses Operanden in die Menge aufgenommen.
- Wenn E ein Klassendurchgriffsoperatorausdruck der Form E1.E2 oder E1.template E2 ist, der einen nicht-statischen Datenmember benennt, werden die potenziellen Ergebnisse von E1 in die Menge aufgenommen.
- Wenn E ein Klassendurchgriffsoperatorausdruck ist, der einen statischen Datenmember benennt, wird der Bezeichnerausdruck, der den Datenmember bezeichnet, in die Menge aufgenommen.
- Wenn E ein Zeiger-auf-Member-Zugriffsausdruck der Form E1.*E2 oder E1.*template E2 ist, dessen zweiter Operand ein Konstantausdruck ist, werden die potenziellen Ergebnisse von E1 in die Menge aufgenommen.
- Wenn E ein Ausdruck in Klammern ((E1)) ist, werden die potenziellen Ergebnisse von E1 in die Menge aufgenommen.
- Wenn E ein glvalue-bedingter Ausdruck ist (E1 ? E2 : E3, wobei E2 und E3 glvalues sind), werden die Vereinigung der potenziellen Ergebnisse von E2 und E3 beide in die Menge aufgenommen.
- Wenn E ein Kommaausdruck ist (E1, E2), sind die potenziellen Ergebnisse von E2 in der Menge der potenziellen Ergebnisse.
- Andernfalls ist die Menge leer.
[bearbeiten] ODR-Verwendung (informelle Definition)
Ein Objekt wird odr-used, wenn sein Wert gelesen (es sei denn, es ist eine Konstante zur Kompilierzeit) oder geschrieben wird, seine Adresse genommen wird oder eine Referenz darauf gebunden wird.
Eine Referenz wird odr-used, wenn sie verwendet wird und ihr Bezugsobjekt nicht zur Kompilierzeit bekannt ist.
Eine Funktion wird odr-used, wenn ein Funktionsaufruf an sie erfolgt oder ihre Adresse genommen wird.
Wenn eine Entität odr-used ist, muss ihre Definition irgendwo im Programm existieren; eine Verletzung davon ist normalerweise ein Linkzeitfehler.
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
[bearbeiten] ODR-Verwendung (formale Definition)
Eine Variable x, die von einem potenziell ausgewerteten Ausdruck expr benannt wird, der an einem Punkt P auftritt, wird von expr odr-used, es sei denn, eine der folgenden Bedingungen ist erfüllt:
- x ist eine Referenz, die an
Pin Konstantausdrücken verwendbar ist. - x ist keine Referenz und (bis C++26)expr ist ein Element der Menge potenzieller Ergebnisse eines Ausdrucks E, und eine der folgenden Bedingungen ist erfüllt:
- E ist ein Ausdruck für verwerfbare Werte, und auf ihn wird keine Lvalue-zu-Rvalue-Konvertierung angewendet.
- x ist ein nicht-flüchtiges(seit C++26) Objekt, das an
Pin Konstantausdrücken verwendbar ist und keine veränderbaren Unterobjekte hat, und eine der folgenden Bedingungen ist erfüllt:
|
(seit C++26) |
- E hat einen nicht-flüchtig qualifizierten Nicht-Klassentyp, und die Lvalue-zu-Rvalue-Konvertierung wird darauf angewendet.
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 ist odr-used, wenn this als potenziell ausgewerteter Ausdruck erscheint (einschließlich des impliziten this in einem nicht-statischen Memberfunktionsaufruf).
|
Eine strukturierte Bindung ist odr-used, wenn sie als potenziell ausgewerteter Ausdruck erscheint. |
(seit C++17) |
Eine Funktion ist in folgenden Fällen odr-used:
- Eine Funktion ist odr-used, wenn sie von einem potenziell ausgewerteten Ausdruck oder einer Konvertierung benannt wird (siehe unten).
- Eine virtuelle Memberfunktion ist odr-used, wenn sie keine reine virtuelle Memberfunktion ist (Adressen von virtuellen Memberfunktionen werden zum Erstellen der vtable benötigt).
- Eine nicht-Placement-Allokations- oder Deallokationsfunktion für eine Klasse ist durch die Definition eines Konstruktors dieser Klasse odr-used.
- Eine nicht-Placement-Deallokationsfunktion für eine Klasse ist durch die Definition des Destruktors dieser Klasse odr-used oder wird durch die Auswahl bei der Auflösung am Punkt der Definition eines virtuellen Destruktors odr-used.
- Ein Zuweisungsoperator in einer Klasse
T, die eine Basis- oder Mitgliedsklasse einer anderen KlasseUist, wird durch implizit definierte Kopierzuweisungs- oder Verschiebezweisungsfunktionen vonUodr-used. - Ein Konstruktor (einschließlich Standardkonstruktoren) für eine Klasse wird durch die Initialisierung, die ihn auswählt, odr-used.
- Ein Destruktor für eine Klasse ist odr-used, wenn er potenziell aufgerufen wird.
| Dieser Abschnitt ist unvollständig Grund: Liste aller Situationen, in denen ODR-Verwendung eine Rolle spielt |
[bearbeiten] Fehlerberichte
Die folgenden Verhaltensändernden Fehlerberichte wurden rückwirkend auf zuvor veröffentlichte C++-Standards angewendet.
| DR | angewendet auf | Verhalten wie veröffentlicht | Korrigiertes Verhalten |
|---|---|---|---|
| CWG 261 | C++98 | eine Deallokationsfunktion für eine polymorphe Klasse kann odr-used sein, auch wenn es keine relevanten new- oder delete-Ausdrücke im Programm gab |
ergänzte die ODR-Verwendungsfälle, um Konstruktoren und Destruktoren abzudecken |
| CWG 678 | C++98 | eine Entität konnte Definitionen haben mit unterschiedlichen Sprachverknüpfungen |
Das Verhalten ist in diesem Fall nicht definiert. |
| CWG 1472 | C++98 | Referenzvariablen, die die Anforderungen für das Erscheinen in einem Konstantausdruck erfüllten, wurden odr-used, selbst wenn die Lvalue-zu-Rvalue-Konvertierung sofort angewendet wurde |
sie sind es nicht in diesem Fall odr-used |
| CWG 1614 | C++98 | das Nehmen der Adresse einer reinen virtuellen Funktion odr-used sie | die Funktion wird nicht odr-used |
| CWG 1741 | C++98 | Konstante Objekte, die in potenziell ausgewerteten Ausdrücken sofort lvalue-zu-rvalue konvertiert wurden, wurden odr-used |
sie werden nicht odr-used |
| CWG 1926 | C++98 | Array-Subskriptausdrücke propagierten keine potenziellen Ergebnisse | sie propagieren |
| CWG 2242 | C++98 | es war unklar, ob ein const-Objekt, das nurin einem Teil seiner Definitionen konstant initialisiert wurde, ODR verletzt |
ODR wird nicht verletzt; das Objekt ist in diesem Fall konstant initialisiert |
| CWG 2300 | C++11 | Lambda-Ausdrücke in verschiedenen Translation Units konnten nie denselben Closure-Typ haben |
der Closure-Typ kann derselbe unter der Ein-Definition-Regel sein |
| CWG 2353 | C++98 | ein statischer Datenmember war kein potenzielles Ergebnis eines Member-Zugriffsausdrucks, der ihn anspricht |
sie ist |
| CWG 2433 | C++14 | ein Variablen-Template konnte nicht haben mehrere Definitionen in einem Programm |
es kann |
[bearbeiten] Referenzen
- C++23 Standard (ISO/IEC 14882:2024)
- 6.3 One definition rule [basic.def.odr]
- C++20 Standard (ISO/IEC 14882:2020)
- 6.3 One definition rule [basic.def.odr]
- C++17 Standard (ISO/IEC 14882:2017)
- 6.2 One definition rule [basic.def.odr]
- C++14 Standard (ISO/IEC 14882:2014)
- 3.2 One definition rule [basic.def.odr]
- C++11 Standard (ISO/IEC 14882:2011)
- 3.2 One definition rule [basic.def.odr]
- C++03-Standard (ISO/IEC 14882:2003)
- 3.2 One definition rule [basic.def.odr]
- C++98 Standard (ISO/IEC 14882:1998)
- 3.2 One definition rule [basic.def.odr]