Не нарушает ли переопределение функции из стандартной библиотеки правило единого определения?

#include <cmath>

double log(double) {return 1.0;}
int main() {
  log(1.0);
}

Предположим, функция log() в <cmath> объявляется в глобальном пространстве имен (это на самом деле не определено, и мы просто делаем это предположение), затем оно ссылается на ту же функцию, что и log() Функция, которую мы определили.
Таким образом, этот код нарушает правило с одним определением (см. Здесь, поскольку диагностика не требуется, этот код может скомпилироваться в каком-либо компиляторе, и мы не можем утверждать, что он верен)?

Примечание. После недавних изменений это не является дубликатом: что такое одно правило определения в C++?

3 ответа

Решение

Типичный сценарий.

Если extern "C" double log(double) первоначально объявлен в глобальном пространстве имен, затем вы повторно объявили его и предоставили определение. Предыдущее упоминание о реализации extern "C" переносит на ваше соответствующее объявление. Ваше определение относится к функции, относящейся к реализации, и это является нарушением ODR.

Что касается проявления UB: это, по-видимому, распространено log как слабый символ линкера. Ваша реализация отменяет libc.so согласно правилам ABI.

(Если реализация не делает extern "C", это все еще в основном все то же самое.)

Другой вероятный сценарий.

Если log объявлен в namespace std а затем перенести в глобальное пространство имен, тогда ваше объявление будет конфликтовать с ним. (На самом деле, using Декларация технически декларация.) Эта ошибка диагностируется.

Предположительно нарушающий сценарий.

тогда это относится к той же функции, что и log Функция, которую мы определили

Один из способов реализации поставить <cmath> имена в глобальном пространстве имен будет объявить extern "C" функции внутри namespace stdтогда делай using namespace stdи убедиться, что это всегда происходит первым делом, когда включен любой стандартный заголовок. поскольку using namespace не "прилипает" - он применяется только к предыдущим объявлениям в назначенном пространстве имен - остальная часть стандартной библиотеки не будет сделана видимой. (Это не будет объявлять имена в глобальном пространстве имен, но стандарт говорит только "помещен в глобальную область имен".)

В такой реализации ваше объявление скрыло бы стандартное и объявило бы новую функцию с новым искаженным именем (скажем, _Z3logd вместо просто log) и новое полное имя (::log вместо ::std::log). Тогда не было бы нарушения ODR (если только некоторая встроенная функция не использует log в одном TU, а другой в другом TU).

Ниже рассматривается предыдущая версия OP. Я оставляю это здесь на случай, если будущие читатели придут сюда с похожим запросом.

Я предполагаю, что два имени ссылаются на одну и ту же сущность тогда и только тогда, когда они имеют одну и ту же декларативную область, где понятие "декларативная область" определено в стандарте [...] Является ли это предположение правильным? Есть ли в стандарте слово, поддерживающее это?

Это называется переменным сокрытием или в разговорной речи. И стандарт говорит то, что вы сказали, почти дословно. §3.3.10 №1 в текущем проекте стандарта C++17:

Имя может быть скрыто явным объявлением того же имени во вложенной декларативной области или производном классе


Таким образом, этот код нарушает правило с одним определением (см. Здесь, поскольку диагностика не требуется, этот код может скомпилироваться в каком-то компиляторе, и мы не можем утверждать, что это правильно)?

Я не буду этого ожидать. Стандарт требует, чтобы все cheader заголовки (и в частности cmath) ввести свои символы в std Пространство имен. Реализация, которая также вливает его в глобальное пространство имен, соответствует стандарту (поскольку стандарт оставляет этот бит как неопределенный), но я бы нашел его в плохой форме. Вы правы, что это могло произойти. Теперь, если вы должны были включить math.h (против мудрого совета), тогда это определенно приведет к нарушению правила одного определения.

Осторожно. ODR касается только определений, которые будут включены в итоговую программу. Это означает, что это не относится к символам, которые могут присутствовать в библиотеках, потому что (нормальный) компоновщик загружает не все библиотеки, а только те части, которые требуются для разрешения символов. Например, в этом коде:

#include <cmath>

double log(double) {return 1.0;}

int main()
{
    log(1.0);
}

Там нет нарушения ODR:

  • либо символ журнала из стандартной библиотеки C был включен только в std пространство имен и нет столкновения вообще
  • или он также включен в глобальное пространство имен

В последнем случае декларация double log(double) не конфликтует с тем из cmath, потому что это то же самое. И как символ log уже определено, его определение из стандартной библиотеки не будет включено в программу. Таким образом, только одно определение для log В программе есть функция, которая: double log(double) {return 1.0;},

Все было бы иначе, если бы вы извлекли объектный модуль, содержащий log из математической библиотеки и явно связать его в вашей программе. Потому что объектные модули всегда включаются в результирующую программу, тогда как объектные модули в библиотеках включаются только условно, если они разрешают неопределенные символы.


Рекомендации от стандарта:

Проекты n3337 для C++11 или n4296 для C++14 (или n4618 для последней ревизии) явно указаны в параграфе 2.2. Этапы перевода [lex.phases]:

§9. Все ссылки на внешние объекты разрешены. Компоненты библиотеки связаны для удовлетворения внешних ссылок на объекты, не определенные в текущем переводе. Весь такой вывод транслятора собирается в образ программы, который содержит информацию, необходимую для выполнения в среде выполнения.

Как показано в коде используется только одна единица перевода и как log в нем уже определено, определение из библиотеки не будет использовано.

Другие вопросы по тегам