Definitions and ODR (One Definition Rule)
Определения являются объявлениями , которые полностью определяют сущность, введённую объявлением. Каждое объявление является определением, за исключением следующих случаев:
- Объявление функции без тела функции:
int f(int); // объявляет, но не определяет f
- Любое объявление с extern спецификатором класса хранения или с спецификатором языковой линковки (таким как extern "C" ) без инициализатора:
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; // implicitly inline, defines S::x }; constexpr int S::x; // declares S::x, not a redefinition |
(since C++17) |
- Объявление имени класса (с помощью forward declaration или с использованием elaborated type specifier в другом объявлении):
struct S; // объявляет, но не определяет S class Y f(class T p); // объявляет, но не определяет Y и T (а также f и p)
enum Color : int; // declares, but does not define 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; // declares, but does not define S2 (S may be incomplete) |
(начиная с C++11) |
using N::d; // объявляет, но не определяет d
|
(since C++17) |
|
(since C++11) |
- Пустая декларация (не определяет никакие сущности)
- using-директива (не определяет никакие сущности)
extern template f<int, char>; // declares, but does not define f<int, char> |
(начиная с C++11) |
- Явная специализация , объявление которой не является определением:
template<> struct A<int>; // объявляет, но не определяет A<int>
Объявление asm не определяет никаких сущностей, но классифицируется как определение.
При необходимости компилятор может неявно определить default constructor , copy constructor , move constructor , copy assignment operator , move assignment operator , и destructor .
Если определение любого объекта приводит к объекту неполного типа или абстрактного типа класса , программа является некорректной.
Содержание |
Правило одного определения
Только одно определение любой переменной, функции, типа класса, типа перечисления , concept (since C++20) или шаблона разрешено в одной единице трансляции (некоторые из них могут иметь несколько объявлений, но разрешено только одно определение).
Только одно определение каждой не- inline функции или переменной, которая odr-used (см. ниже), должно присутствовать во всей программе (включая любые стандартные и пользовательские библиотеки). Компилятор не обязан диагностировать это нарушение, но поведение программы, нарушающей это правило, является неопределенным.
Для встроенной функции или встроенной переменной (начиная с C++17) , определение требуется в каждой единице трансляции, где она odr-used .
Для класса определение требуется везде, где класс используется таким образом, что требуется его полное определение.
В программе может быть более одного определения для каждого из следующих: тип класса, тип перечисления, встраиваемая функция , встраиваемая переменная (since C++17) , шаблонная сущность (шаблон или член шаблона, но не полная специализация шаблона ), при условии что все следующие условия выполняются:
- Каждое определение появляется в отдельной единице трансляции.
|
(начиная с C++20) |
- Каждое определение состоит из одинаковой последовательности токенов (обычно присутствует в одном и том же заголовочном файле).
- Поиск имени внутри каждого определения находит те же сущности (после разрешения перегрузки ), за исключением случаев, когда:
-
- Константы с внутренней или без компоновки могут ссылаться на разные объекты, при условии что они не являются ODR-используемыми и имеют одинаковые значения в каждом определении.
|
(since C++11) |
- Перегруженные операторы, включая функции преобразования, выделения и освобождения памяти, ссылаются на одну и ту же функцию из каждого определения (если не ссылаются на ту, что определена внутри определения).
- Соответствующие сущности имеют одинаковую языковую линковку в каждом определении (например, включаемый файл не находится внутри блока extern "C" ).
-
Если
constобъект константно-инициализирован в любом из определений, он константно-инициализирован в каждом определении. - Вышеуказанные правила применяются к каждому аргументу по умолчанию, используемому в каждом определении.
- Если определение предназначено для класса с неявно объявленным конструктором, каждая единица трансляции, где он ODR-используется, должна вызывать один и тот же конструктор для базового класса и членов.
|
(since C++20) |
- Если определение является шаблоном, то все эти требования применяются как к именам в точке определения, так и к зависимым именам в точке инстанцирования.
Если все эти требования выполнены, программа ведет себя так, как если бы в ней существовало только одно определение. В противном случае программа является некорректной, диагностика не требуется.
Примечание: в языке C не существует общепрограммного правила одного определения (ODR) для типов, и даже extern-объявления одной и той же переменной в разных единицах трансляции могут иметь разные типы при условии их совместимости . В C++ исходные токены, используемые в объявлениях одного и того же типа, должны быть идентичными, как описано выше: если один .cpp файл определяет struct S { int x ; } ; , а другой .cpp файл определяет struct S { int y ; } ; , то поведение программы, связывающей их вместе, не определено. Эта проблема обычно решается с помощью безымянных пространств имён .
Наименование сущности
Переменная именуется выражением, если выражение является идентифицирующим выражением, которое обозначает её.
Функция называется выражением или преобразованием в следующих случаях:
- Функция, имя которой появляется в виде выражения или преобразования (включая именованную функцию, перегруженный оператор, пользовательское преобразование , пользовательские формы размещения operator new , нестандартная инициализация) считается названной этим выражением, если она выбрана разрешением перегрузки, за исключением случаев, когда это неквалифицированная чисто виртуальная функция-член или указатель-на-член на чисто виртуальную функцию.
- Функция выделения или освобождения памяти для класса считается названной выражением new , появляющимся в выражении.
- Функция освобождения памяти для класса считается названной выражением delete , появляющимся в выражении.
- Конструктор, выбранный для копирования или перемещения объекта, считается названным выражением или преобразованием, даже если происходит исключение копирования . Использование prvalue в некоторых контекстах не копирует и не перемещает объект, см. обязательное исключение . (начиная с C++17)
Потенциально вычисляемое выражение или преобразование odr-использует функцию, если называет её.
|
Выражение или преобразование, потенциально вычисляемое в контексте констант, которое ссылается на constexpr-функцию, делает её необходимой для константного вычисления , что вызывает определение функции по умолчанию или инстанцирование специализации шаблона функции, даже если выражение не вычисляется. |
(since C++11) |
Возможные результаты
Множество потенциальных результатов выражения E представляет собой (возможно, пустое) множество выражений-идентификаторов, которые встречаются внутри 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 являются glvalue), объединение потенциальных результатов E2 и E3 включаются в множество.
- Если E является выражением с запятой ( E1, E2 ), потенциальные результаты E2 входят в множество потенциальных результатов.
- В противном случае множество пусто.
Использование 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-контексте здесь: требуется определение
Использование ODR (формальное определение)
Переменная
x
, именуемая
потенциально вычисляемым выражением
expr
, которое появляется в точке
P
, является odr-используемой выражением
expr
, если не выполняется ни одно из следующих условий:
-
x
является ссылкой, которая
может использоваться в константных выражениях
в точке
P. -
x
не является ссылкой и
(до C++26)
expr
является элементом множества потенциальных результатов выражения
E
, и выполняется любое из следующих условий:
- E является discarded-value выражением , и к нему не применяется преобразование lvalue-to-rvalue.
-
x
является
не-volatile
(начиная с C++26)
объектом, который может использоваться в константных выражениях в точке
Pи не имеет изменяемых подобъектов, и выполняется любое из следующих условий:
|
(начиная с C++26) |
-
-
- E имеет не-volatile-квалифицированный не-классовый тип, и к нему применяется преобразование lvalue-to-rvalue.
-
struct S { static const int x = 1; }; // применение lvalue-to-rvalue преобразования // к S::x дает константное выражение int f() { S::x; // выражение с отброшенным значением не использует S::x в odr-смысле return S::x; // выражение, где применяется lvalue-to-rvalue преобразование // не использует S::x в odr-смысле }
* this является odr-используемым, если this появляется как потенциально вычисляемое выражение (включая неявный this в выражении вызова нестатической функции-члена).
|
Структурированная привязка считается odr-используемой, если она появляется в потенциально вычисляемом выражении. |
(начиная с C++17) |
Функция является odr-используемой в следующих случаях:
- Функция является odr-используемой, если она названа (см. ниже) потенциально вычисляемым выражением или преобразованием.
- Виртуальная функция-член является odr-используемой, если она не является чисто виртуальной функцией-членом (адреса виртуальных функций-членов требуются для построения vtable).
- Функция неразмещающего выделения или освобождения памяти для класса является odr-используемой определением конструктора этого класса.
- Функция неразмещающего освобождения памяти для класса является odr-используемой определением деструктора этого класса или выбором при поиске в точке определения виртуального деструктора.
-
Оператор присваивания в классе
T, который является членом или базовым классом другого классаU, является odr-используемым неявно определенными функциями копирующего или перемещающего присваиванияU. - Конструктор (включая конструкторы по умолчанию) для класса является odr-используемым инициализацией , которая его выбирает.
- Деструктор для класса является odr-используемым, если он потенциально вызывается .
|
Этот раздел не завершён
Причина: перечень всех ситуаций, когда odr-use имеет значение |
Отчеты о дефектах
Следующие отчеты об изменениях поведения, влияющие на дефекты, были применены задним числом к ранее опубликованным стандартам C++.
| DR | Применяется к | Поведение в опубликованной версии | Корректное поведение |
|---|---|---|---|
| CWG 261 | C++98 |
функция освобождения для полиморфного класса
могла быть odr-использована даже при отсутствии соответствующих выражений new или delete в программе |
дополнены случаи
odr-использования для конструкторов и деструкторов |
| CWG 678 | C++98 |
сущность могла иметь определения
с различными языковыми связями |
поведение является
неопределённым в этом случае |
| CWG 1472 | C++98 |
ссылочные переменные, удовлетворяющие требованиям для
появления в константном выражении, были odr-использованы даже если преобразование lvalue-to-rvalue применяется немедленно |
они не являются
odr-использованными в этом случае |
| CWG 1614 | C++98 | взятие адреса чистой виртуальной функции odr-использовало её | функция не является odr-использованной |
| CWG 1741 | C++98 |
константные объекты, которые немедленно преобразуются
lvalue-to-rvalue в потенциально вычисляемых выражениях, были odr-использованы |
они не являются odr-использованными |
| CWG 1926 | C++98 | выражения индексации массива не распространяли потенциальные результаты | они распространяют |
| CWG 2242 | C++98 |
было неясно, нарушает ли
const
объект, который только
частично константно-инициализирован в своих определениях, ODR |
ODR не нарушен; объект является
константно-инициализированным в этом случае |
| CWG 2300 | C++11 |
лямбда-выражения в различных единицах трансляции
никогда не могли иметь одинаковый тип замыкания |
тип замыкания может быть
одинаковым согласно правилу одного определения |
| 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]