Spec-Zone.ru › C++

Определения и ODR (правило одного определения)

Определения — это объявления, которые полностью определяют сущность, вводимую объявлением. Каждое объявление является определением, за исключением следующих случаев:

  • Объявление функции без тела функции:
int f(int); // declares, but does not define f
  • Любое объявление со extern спецификатором класса хранения или со спецификатором связывания языка (например, extern"C") без инициализатора:
extern const int a;     // declares, but does not define a
extern const int b = 1; // defines b
  • Объявление нестатического(начиная с C++17) статического члена данных внутри определения класса:
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:
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:
using N::d; // declares, but does not define d
  • Объявление руководства по выводу (не определяет никаких сущностей)
(начиная с C++17)
  • Объявление static_assert (не определяет никаких сущностей)
  • Объявление атрибутов (не определяет никаких сущностей)
(начиная с C++11)
  • Пустое объявление (не определяет никаких сущностей)
  • Директива using (не определяет никаких сущностей)
  • Объявление явной инстанциации («extern template»):
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 (см. ниже). Компилятор не обязан диагностировать это нарушение, но поведение программы, в которой оно допущено, не определено.

Для inline-функции или inline-переменной(начиная с C++17) определение требуется в каждой единице трансляции, где она используется по ODR .

Для класса определение требуется во всех случаях, когда класс используется способом, требующим, чтобы он был полным.

В программе может быть более одного определения каждого из следующих элементов: типа класса, типа перечисления, inline-функции, inline-переменной(начиная с C++17), шаблонной сущности (шаблона или члена шаблона, но не полной специализации шаблона), если выполнены все следующие условия:

  • Каждое определение находится в отдельной единице трансляции.
  • Определения не принадлежат именованному модулю.
(начиная с C++20)
  • Каждое определение состоит из одной и той же последовательности токенов (обычно определения находятся в одном и том же заголовочном файле).
  • Поиск имён из каждого определения находит одни и те же сущности (после разрешения перегрузки), за исключением следующих случаев:
    • Константы с внутренним связыванием или без связывания могут ссылаться на разные объекты, если они не используются по ODR и имеют одинаковые значения во всех определениях.
  • Лямбда-выражения, не находящиеся в аргументе по умолчанию или аргументе шаблона по умолчанию(начиная с C++20), однозначно идентифицируются последовательностью токенов, использованных для их определения.
(начиная с C++11)
  • Перегруженные операторы, включая функции преобразования, выделения и освобождения памяти, ссылаются в каждом определении на одну и ту же функцию (если только речь не идёт о функции, определённой внутри самого определения).
  • Соответствующие сущности имеют одинаковое связывание языка в каждом определении (например, включаемый файл не находится внутри блока extern"C").
  • Если объект const инициализируется константным выражением посредством константной инициализации в одном из определений, он должен инициализироваться константным выражением во всех определениях.
  • Приведённые выше правила применяются ко всем аргументам по умолчанию, используемым в каждом определении.
  • Если определение относится к классу с неявно объявленным конструктором, каждая единица трансляции, в которой он используется по ODR, должна вызывать один и тот же конструктор для базового класса и членов.
  • Если определение относится к классу с неявно объявленным оператором трёхстороннего сравнения, каждая единица трансляции, в которой он используется по ODR, должна вызывать один и тот же оператор сравнения для базового класса и членов.
(начиная с 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-функцию, делает её необходимой для вычисления константного выражения. Это приводит к определению функции, объявленной как defaulted, или инстанциации специализации шаблона функции, даже если выражение не вычисляется.

(начиная с 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; // 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 (формальное определение)

Переменная x, упоминаемая потенциально вычисляемым выражением expr, которое встречается в точке P, используется по ODR выражением expr, если не выполнено ни одно из следующих условий:

  • x является ссылкой, допустимой в константных выражениях в точке P.
  • x не является ссылкой, и (до C++26)expr является элементом множества потенциальных результатов выражения E, и выполняется одно из следующих условий:
    • E является выражением с отброшенным значением, к которому не применяется преобразование lvalue-to-rvalue.
    • x является невольтильным(начиная с C++26) объектом, допустимым в константных выражениях в точке P и не имеющим изменяемых подобъектов, и выполняется одно из следующих условий:
  • E является выражением доступа к члену класса, обозначающим нестатический член данных ссылочного типа, а тип выражения объекта не имеет квалификатора volatile.
(начиная с C++26)
  • E имеет неклассовый тип без квалификатора volatile, и к нему применяется преобразование lvalue-to-rvalue.
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 используется по ODR, если this встречается как потенциально вычисляемое выражение (включая неявное this в выражении вызова нестатической функции-члена).

Структурированная привязка используется по ODR, если она встречается как потенциально вычисляемое выражение.

(начиная с C++17)

Функция используется по ODR в следующих случаях:

  • Функция используется по ODR, если она упоминается (см. ниже) потенциально вычисляемым выражением или преобразованием.
  • Виртуальная функция-член используется по ODR, если она не является чисто виртуальной функцией-членом (адреса виртуальных функций-членов необходимы для построения таблицы виртуальных функций).
  • Функция выделения или освобождения памяти класса, не являющаяся размещающей, используется по ODR определением конструктора этого класса.
  • Функция освобождения памяти класса, не являющаяся размещающей, используется по ODR определением деструктора этого класса или в случае, когда она выбрана поиском в точке определения виртуального деструктора.
  • Оператор присваивания класса T, являющийся членом или базовым классом другого класса U, используется по ODR неявно определёнными операторами присваивания копированием или перемещением класса U.
  • Конструктор класса (включая конструкторы по умолчанию) используется по ODR в ходе инициализации, которая его выбирает.
  • Деструктор класса используется по ODR, если он потенциально вызывается.

Отчёты об ошибках

Следующие отчёты об ошибках, изменяющие поведение, были ретроактивно применены к ранее опубликованным стандартам 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 не было ясно, нарушает ли ODR объект const,
который константно инициализируется лишь в части своих определений
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]

© cppreference.com
Licensed under the Creative Commons Attribution-ShareAlike Unported License v3.0.
https://en.cppreference.com/cpp/language/definition

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API