Если у вас есть две классические единицы перевода, которые определяют один и тот же символ (скажем, auto fun0() -> void
), я получаю fatal error LNK1169: one or more multiply defined symbols found
в MSVC, поскольку это нарушает ODR.
Одним из первых шагов, который я сделал с модулями C++, было тестирование поведения с этим основным принципом. Итак, у нас есть два файла модуля (module0.ixx
и module1.ixx
) с почти идентичным содержимым:
// module0.ixx
export module module0;
import <cstdio>;
export void f_test() { printf("f_test()\n"); }
// module1.ixx
export module module1;
import <cstdio>;
export void f_test() { printf("f_test()\n"); }
В моем main.cpp
я делаю
import module0;
import module1;
auto main() -> int{
f_test();
}
К моему удивлению, это компилируется просто отлично. С этим возникают ожидаемые проблемы: если определение отличается, поведение зависит от порядка и т. д. Ожидается ли это? Это было 5 минут игры с модулями и кажется довольно запутанным.
«Множественно определенные символы больше не являются ошибкой?» - Они никогда не были обязаны быть ошибкой. Нарушение ODR является «неправильным; диагностика не требуется». Что в основном означает, что возникает неопределенное поведение, как вы видите здесь.
Эта проблема не характерна для модулей. «Старый C++» может демонстрировать такое же поведение.
// oops_header.h
#ifndef OOPS
#error OOPS
#endif
#include <iostream>
inline void oops_func() { std::cout << OOPS; }
А мы такие же махинации когда включаем и возимся с макросом
// tu1.cpp
#define OOPS 123
#include <oops_header.h>
void a() { oops_func(); }
// tu2.cpp
#define OOPS 42
#include <oops_header.h>
void b() { oops_func(); }
Где основная функция, подобная этой
extern void a();
extern void b();
int main() {
a();
b();
}
Будут отображаться те же проблемы, с которыми вы столкнулись с модулями. Вывод будет зависеть от звездочек, потому что я нарушил ODR (встроенные функции требуют от ODR идентичности на уровне токена).
Вы видите это с модулями из-за артефакта их сегодняшней реализации (что-то вроде предварительно скомпилированных заголовков).
И да, я изо всех сил старался сломать ODR «необнаруживаемым» способом, но это только потому, что нет необходимости его обнаруживать. Я нарушил соглашение, по которому компилятор доверял мне вести себя разумно.
Я действительно этого не знал. Это разочаровывает, но приятно знать. Спасибо
Тем не менее, реализации C++ обычно предоставляют диагностику для многократно определенных невстроенных функций и переменных "старого C++" с внешней связью. Это регресс, независимо от того, что говорит стандарт.
@н.м. - Невозможно регрессировать, когда вещи раньше не существовало. TU и модульные блоки взаимодействуют... не совсем очевидным образом, я бы сказал.
@н.м. -- повторяющиеся имена в отдельных библиотеках обычно не вызывают сообщений об ошибках. Например, это обычный механизм замены глобальных operator new
и operator delete
.
@PeteBecker Это вообще не работает как замена глобального механизма создания/удаления. Если вы добавите void f0() { f_test(); }
к module0
и void f1() { f_test(); }
к module1
и вызовете f0
и f1
из main
, f0
вызовет f_test
из module0
, f1
вызовет f_test
из module1
, а main
вызовет тот f_test
, который будет импортирован первым (аналогично Windows DLL). Это довольно прискорбно.
@н.м. -- Я говорил о том, как работает связывание в том, что вы называете "старым C++"; после разрешения символа компоновщики обычно игнорируют повторяющиеся определения в библиотечных модулях, которые иначе не были загружены.
@PeteBecker, если библиотечный модуль (объект на языке компоновщика) не вставлен, он просто не является частью программы, и ODR к нему не применяется.
@н.м. -- то есть в лучшем случае глянец; в стандарте ничего не говорится о библиотеках (в этом смысле), и связывание с библиотекой делает имена в этой библиотеке видимыми для компоновщика. В стандарте ничего не говорится о том, что компоновщик не должен сообщать об ошибке для такой программы. Здесь, конечно, много традиций, и любой компоновщик, который отказался бы связать эту программу, не был бы вообще популярен. <g> Но это далеко от вашего вопроса.
@PeteBecker В какой-то момент вам нужно решить, что является частью вашей программы, а что нет. Стандарт не диктует это решение. Если вы решите, что вся библиотека, которую вы передаете компоновщику, является частью программы, то вы должны избегать использования всех имен, определенных несвязанными частями этой библиотеки, иначе ваша программа будет иметь UB без какой-либо веской причины.
@н.м. -- как компилятор/компоновщик узнает, что вы решили? Обычно вы не указываете компоновщику, какие части библиотеки использовать; он просматривает всю библиотеку, чтобы увидеть, что полезно.
@PeteBecker Вы можете решить, что все, что на самом деле сделал компоновщик, является частью программы, а все, что не было, не является. (Вы знаете, как компоновщик загружает данные, это описано в руководстве; вы можете восстановить список вручную, если хотите). Или у вас может быть какая-то альтернативная процедура принятия решения. Я не знаю ни одного здравомыслящего.
FWIW, clang разрешает несколько определений в разных модулях только в том случае, если определения одинаковы.