What ODR hazard arises when mixing #include and import of the same code?
A team wraps an existing header in a module but leaves the old #include in place across the codebase. The build links and the program runs — for now.
// 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
Identify the bug and explain the cause.
If one TU #includes a header while another imports a module wrapping the same code, the same entity gets two definitions through different paths. They must be token-for-token identical; any drift between header and module is an ODR violation, often silent and unchecked.
- ✗Believing the linker reliably detects and rejects clashing definitions from the two paths
- ✗Assuming the ODR violation is always a hard compile error rather than silent UB
- ✗Thinking
importand#includeof the same code are guaranteed to be equivalent
- →Why is an ODR violation often undiagnosed rather than a hard error?
- →What discipline keeps a header and its module wrapper from drifting apart?
Bug scenario
A team wraps an existing header in a module but does not remove the old #include across the code.
// 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 definition via text
// physics.cpp — new path
import geometry; // Point/norm definition via BMI
Now Point and norm are defined twice — once by textual substitution, once from the BMI. As long as both paths yield a token-for-token identical definition the program works. But it is fragile.
When it blows up
Someone edits geometry.h — adds a double z; field to Point. Files using import geometry see the new definition only after the BMI is rebuilt; files using #include pick up the edit immediately. Between builds, two TUs see a Point of different size.
This is an ODR violation. The linker usually does not catch it: it sees one symbol and silently picks one definition. The result is runtime memory corruption, not a build error.
The fix
Pick one path per unit of code. The most robust option is to stop publishing geometry.h: once wrapped in a module, every consumer switches to import geometry, and the header survives only inside the module's global fragment.