Какая ODR-опасность возникает при смешивании #include и import одного и того же кода?
Команда оборачивает существующий заголовок в модуль, но оставляет старый #include по всему коду. Сборка линкуется и программа пока работает.
// geometry.h — old header, still in the repo
struct Point { double x, y; };
inline double norm(Point p) { return p.x*p.x + p.y*p.y; }
// geometry.cppm — new module wrapper
module;
#include "geometry.h"
export module geometry;
export using ::Point;
export using ::norm;
// renderer.cpp — old path
#include "geometry.h" // Point/norm via text substitution
// physics.cpp — new path
import geometry; // Point/norm via the BMI
Найдите ошибку и объясните причину.
Если одна TU делает #include заголовка, а другая — import модуля, оборачивающего тот же код, одна сущность получает два определения разными путями. Они должны быть токен-в-токен идентичны; любое расхождение между заголовком и модулем — это нарушение ODR, часто молчаливое и непроверяемое.
- ✗Полагать, что линковщик надёжно обнаруживает и отвергает конфликтующие определения двух путей
- ✗Считать, что нарушение ODR — всегда жёсткая ошибка компиляции, а не молчаливый UB
- ✗Думать, что
importи#includeодного кода гарантированно эквивалентны
- →Почему нарушение ODR часто не диагностируется, а не даёт жёсткую ошибку?
- →Какая дисциплина не даёт заголовку и его модульной обёртке разойтись?
Сценарий бага
Команда оборачивает существующий заголовок в модуль, но не удаляет старый #include по всему коду.
// geometry.h — старый заголовок, всё ещё в репозитории
struct Point { double x, y; };
inline double norm(Point p) { return p.x*p.x + p.y*p.y; }
// geometry.cppm — новая модульная обёртка
module;
#include "geometry.h"
export module geometry;
export using ::Point;
export using ::norm;
// renderer.cpp — старый путь
#include "geometry.h" // определение Point/norm через текст
// physics.cpp — новый путь
import geometry; // определение Point/norm через BMI
Теперь Point и norm определены дважды — один раз текстовой подстановкой, один раз из BMI. Пока оба пути дают токен-в-токен идентичное определение, программа работает. Но это хрупко.
Когда это взрывается
Кто-то правит geometry.h — добавляет поле double z; в Point. Файлы, использующие import geometry, видят новое определение только после пересборки BMI; файлы с #include подхватывают правку сразу. Между сборками две TU видят Point разного размера.
Это нарушение ODR. Линковщик его обычно не ловит: он видит один символ и молча выбирает одно определение. Результат — повреждение памяти в рантайме, а не ошибка сборки.
Исправление
Выберите один путь на единицу кода. Самое надёжное — не держать geometry.h публичным: после обёртки в модуль все потребители переходят на import geometry, а заголовок остаётся только внутри глобального фрагмента модуля.