Модули С++: умножение определенных символов больше не является ошибкой?

Если у вас есть две классические единицы перевода, которые определяют один и тот же символ (скажем, 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 минут игры с модулями и кажется довольно запутанным.

FWIW, clang разрешает несколько определений в разных модулях только в том случае, если определения одинаковы.

n. m. 14.02.2023 10:47
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
1
81
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

«Множественно определенные символы больше не являются ошибкой?» - Они никогда не были обязаны быть ошибкой. Нарушение 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 «необнаруживаемым» способом, но это только потому, что нет необходимости его обнаруживать. Я нарушил соглашение, по которому компилятор доверял мне вести себя разумно.

StoryTeller - Unslander Monica 14.02.2023 09:47

Я действительно этого не знал. Это разочаровывает, но приятно знать. Спасибо

Basti 14.02.2023 09:50

Тем не менее, реализации C++ обычно предоставляют диагностику для многократно определенных невстроенных функций и переменных "старого C++" с внешней связью. Это регресс, независимо от того, что говорит стандарт.

n. m. 14.02.2023 12:31

@н.м. - Невозможно регрессировать, когда вещи раньше не существовало. TU и модульные блоки взаимодействуют... не совсем очевидным образом, я бы сказал.

StoryTeller - Unslander Monica 14.02.2023 12:32

@н.м. -- повторяющиеся имена в отдельных библиотеках обычно не вызывают сообщений об ошибках. Например, это обычный механизм замены глобальных operator new и operator delete.

Pete Becker 14.02.2023 15:29

@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). Это довольно прискорбно.

n. m. 14.02.2023 17:19

@н.м. -- Я говорил о том, как работает связывание в том, что вы называете "старым C++"; после разрешения символа компоновщики обычно игнорируют повторяющиеся определения в библиотечных модулях, которые иначе не были загружены.

Pete Becker 14.02.2023 17:53

@PeteBecker, если библиотечный модуль (объект на языке компоновщика) не вставлен, он просто не является частью программы, и ODR к нему не применяется.

n. m. 14.02.2023 18:27

@н.м. -- то есть в лучшем случае глянец; в стандарте ничего не говорится о библиотеках (в этом смысле), и связывание с библиотекой делает имена в этой библиотеке видимыми для компоновщика. В стандарте ничего не говорится о том, что компоновщик не должен сообщать об ошибке для такой программы. Здесь, конечно, много традиций, и любой компоновщик, который отказался бы связать эту программу, не был бы вообще популярен. <g> Но это далеко от вашего вопроса.

Pete Becker 14.02.2023 19:04

@PeteBecker В какой-то момент вам нужно решить, что является частью вашей программы, а что нет. Стандарт не диктует это решение. Если вы решите, что вся библиотека, которую вы передаете компоновщику, является частью программы, то вы должны избегать использования всех имен, определенных несвязанными частями этой библиотеки, иначе ваша программа будет иметь UB без какой-либо веской причины.

n. m. 14.02.2023 20:33

@н.м. -- как компилятор/компоновщик узнает, что вы решили? Обычно вы не указываете компоновщику, какие части библиотеки использовать; он просматривает всю библиотеку, чтобы увидеть, что полезно.

Pete Becker 14.02.2023 21:36

@PeteBecker Вы можете решить, что все, что на самом деле сделал компоновщик, является частью программы, а все, что не было, не является. (Вы знаете, как компоновщик загружает данные, это описано в руководстве; вы можете восстановить список вручную, если хотите). Или у вас может быть какая-то альтернативная процедура принятия решения. Я не знаю ни одного здравомыслящего.

n. m. 15.02.2023 04:21

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