Для функции, объявленной вперед, разрешен неправильный тип возвращаемого значения: почему здесь нет ошибки компоновщика?

$ g++ --version
Configured with: --prefix=/Library/Developer/CommandLineTools/usr --with-gxx-include-dir=/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/4.2.1
Apple clang version 12.0.0 (clang-1200.0.32.29)
Target: x86_64-apple-darwin23.4.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin

а.cc

#include<iostream>

using namespace std;

static int x = 5053;

void f2();

int main() {
  cout << "a: " << x << endl;
  f2();
  return 0;
}

b.cc

#include<iostream>

using namespace std;

static int x = 4921;

string f2() {
  cout << "b: " << x << endl;
  return "";
}

Выход

$ g++ --std=c++17 a.cc b.cc && ./a.out
a: 5053
b: 4921

Почему я смог переслать объявление string f2(); из b.cc как void f2(); в a.cc?

Будем признательны за любые ссылки на cppreference или спецификации, позволяющие это сделать.

попробуй сделать extern void f2();

kesarling 01.05.2024 05:01

Включите все предупреждения. Вероятно, происходит следующее: вызывается первое доступное совпадение, которое оказывается string f2, потому что void f2 не определено.

kesarling 01.05.2024 05:11
g++ --std=c++17 a.cpp b.cpp && ./a.out /usr/bin/ld: /tmp/ccZVfIKp.o: in function main': a.cpp:(.text+0x47): неопределенная ссылка на f2()' collect2: error: ld returned 1 exit status Вот что я получил
kesarling 01.05.2024 05:14

@kesarlingHe-Him <br> cat a.cc | grep extern <br> extern void f2();. g++ -Wall --std=c++17 a.cc b.cc && ./a.out <br> a: 5053 b: 4921

ps_ 01.05.2024 05:14

Как вы думаете, что делает cat a.cc | grep extern extern void f2();?

kesarling 01.05.2024 05:15

Программа здесь не работает. Получаем ошибку компоновщика.

user12002570 01.05.2024 05:20

@kesarlingHe-Him Функции уже есть extern. Не нужно писать extern void f2();

user12002570 01.05.2024 05:20

@ user12002570, я очень надеюсь, что ты не говоришь, что string f2 — это перегрузка void f2.

kesarling 01.05.2024 05:21

@user12002570 если это так, то объясните это

kesarling 01.05.2024 05:23

@kesarlingHe-Him Мы действительно должны получить ошибку компоновщика, как показано в демо . Тем не менее обратите внимание, что вы можете не писать extern здесь с помощью f2

user12002570 01.05.2024 05:24

@kesarlingHe-Him Просто пытался показать, что добавление extern не привело для меня к каким-либо предупреждениям/ошибкам с g++ -Wall. Однако, кстати, с -Weverything, независимо от наличия extern в предварительном объявлении в a.cc, я получаю -Wmissing-prototypes, т. е. b.cc:7:8: warning: no previous prototype for function 'f2' [-Wmissing-prototypes] .

ps_ 01.05.2024 05:27

@ps_ И gcc, и clang выдают ошибку компоновщика. Демо

user12002570 01.05.2024 05:28

@ps_, вот и все! Я думаю это как-то связано с ОС. Некоторые ОС добавляют нестандартные функции, ведущие к ужасным практикам кодирования (помните int a[n]; или #include <bits/stdc++.h>?)

kesarling 01.05.2024 05:31

@user12002570 user12002570 «Мы действительно должны получить ошибку компоновщика». Нет, не должны. Возможно, нам так повезет, что иногда, в очень особых обстоятельствах, мы получаем ошибку компоновщика.

n. m. could be an AI 01.05.2024 06:36

@n.m.couldbeanAI Да, я знаю, что это не ошибка. Почему-то забыл, что здесь диагностика не требуется. Я обновил ответ, чтобы отразить то же самое. Спасибо, что напомнили мне удалить часть «это ошибка».

user12002570 01.05.2024 09:08
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
5
15
128
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Будем признательны за любые ссылки на cppreference или спецификации, позволяющие это сделать.

Программа имеет неопределенное поведение, поскольку вы использовали f2, но не предоставили никакого определения для версии void f2();. По сути, нам необходимо предоставить одно и только одно определение невстроенной функции, используемой в odr.

Это видно из ODR:

Во всей программе (включая любые стандартные и пользовательские библиотеки) должно присутствовать одно и только одно определение каждой невстроенной функции или переменной, используемой odr (см. ниже). Компилятор не обязан диагностировать это нарушение, но поведение программы, которая его нарушает, не определено.

Как видите, реализация для этого не обязана выдавать диагностику.

В опубликованной демонстрации в main.cpp, если я изменю предварительное объявление на string f2();, оно все равно выдаст ошибку компоновщика: godbolt.org/z/TxcKPYb54. Тем не менее, g++ (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0 выдает ошибку компоновщика, которая исправляется, если я изменяю предварительное объявление, чтобы оно имело правильный тип возвращаемого значения. Итак, похоже на проблему с моей версией яблочного звона. Не уверен, исправлено ли это в новых версиях Apple Clang.

ps_ 01.05.2024 05:43

@ps_ Если вы измените void f2() на string f2() в main.cpp, это начнет работать. См. модифицированную демонстрацию, где мы не получаем ошибок компоновщика. Должно быть, какая-то проблема с настройками Godbolt.

user12002570 01.05.2024 05:50

@ps_ См. обновленный ответ, где я добавил ссылку на cppreference, которая объясняет, что это неопределенное поведение, поскольку вы должны предоставить одно и только одно определение невстроенной функции, которая используется odr. Нарушение приводит к UB и компилятор не обязан его диагностировать.

user12002570 01.05.2024 06:08

Это не ошибка. Поведение неопределенное, поэтому может случиться что угодно. Вызов «неправильной» функции попадает в область «чего угодно». Ваш пример с godbolt не удался, потому что libstdc++ обрабатывает std::string особым образом. Измените string на vector<char> и посмотрите, что произойдет, или используйте libc++ и посмотрите, что произойдет.

n. m. could be an AI 01.05.2024 06:25

Я думаю, что вид нарушений ODR - это неправильно сформированный отчет о недоставке?

HolyBlackCat 01.05.2024 07:26

@HolyBlackCat это действительно так.

n. m. could be an AI 01.05.2024 07:33

@n.m.couldbeanAI Да, когда я обновил ответ, указав, что это UB (или IFNDR), я забыл удалить из ответа часть «это ошибка».

user12002570 01.05.2024 09:06
Ответ принят как подходящий

Мы не всегда получаем ошибку компоновщика, поскольку стандарт C++ не требует ошибки компоновщика или любой другой ошибки в этом случае, а реализации в большинстве случаев не делают ничего лишнего.

Стандарт не требует наличия ошибок, поскольку общая технология компоновщика не позволяет реализации обнаруживать такие ошибки.

Страуструп решил исключить из C++ искажение имен тип возвращаемого значения, чтобы имена двух функций, которые различаются только типом возвращаемого значения, преобразулись в одно и то же имя символа. Благодаря этому некоторые ошибки, связанные с нарушением ODR, остаются незамеченными. Если заставить возвращаемый тип участвовать в искажении, другие ошибки нарушения ODR останутся незамеченными.

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

Компилятор, который вы используете, делает именно это. Итак, это что-то вроде _Z2f2v в объектном файле. Вы можете увидеть это, набрав nm a.o | grep f2 и nm b.o | grep f2 или посмотрев выходные данные сборки в проводнике компилятора (используйте clang, добавьте -stdlib=libc++ к параметрам и снимите флажок «снять символы» в меню). Нигде нет указаний на то, что функция должна возвращать какой-либо конкретный тип.

Так почему же тогда gcc обнаруживает эту ошибку?

Это потому, что иногда gcc включает тип возвращаемого значения в искажение имени, и ваша программа просто попадает в этот особый случай. Это связано с большой поломкой ABI в C++11. gcc и libstdc++ пришлось изменить макет некоторых классов стандартной библиотеки, в частности std::string (также std::list, но эй, кто это использует?). Итак, авторы gcc сделали умную вещь, чтобы сохранить обратную совместимость: они изменили искажение всего, что включает в себя эти классы - -- и на этот раз и в возвращаемом типе. Таким образом, старый код ABI не может без ошибок связываться с новым кодом ABI. Имена не будут совпадать.

Вы можете убедиться в этом, вызвав nm для объектов, скомпилированных с помощью g++ , или снова зайдя в проводник компилятора . Вы увидите, что объект, который упоминает void f2(), по-прежнему содержит что-то вроде _Z2f2v, но объект, который упоминает string f2(), имеет что-то вроде _Z2f2B5cxx11v (обратите внимание на cxx11 в имени — таким образом мы знаем, что имеем дело с новым пост-C++). 11 ПАБИ). Все бы хорошо, но если вы используете обе функции в одной и той же программе (в разных единицах трансляции), компилятор этого не обнаружит, что является еще одним необнаружимым нарушением ODR (все еще разрешенным стандартом).

Этого не происходит с другими типами, поэтому, если вы измените f2 на возврат, скажем, std::vector<char>, вы не получите ошибку компоновщика ни с одним компилятором.

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