C++ ODR is not just "Undefined Behaviour" it's an example of IFNDR, a get-out in which the language standard just washes its hands of difficult problems by saying in that case what you've written is silently not a C++ program, so its behaviour is arbitrary but you don't get any compiler diagnostics.
Rust doesn't have this get-out. To some extent that's because it's not a formal standard, but mostly it's because IFNDR is diametrically opposed to correctness. Many C++ programs (perhaps even the vast majority) technically have no meaning whatsoever due to IFNDR which is bad -- it's not that the C++ compiler judges them to be correct when they aren't, but that it has no opinion either way, ever and programmers mistake "it compiled" for success.
Thus, in Rust you don't get "ODR errors" here, your program won't compile.
Here's a nice easy Rust semver break example: I make a library about US vacation booking, it has a public enumerated type FederalHoliday but initially that enum doesn't mention Juneteenth - whoops. Somebody points that out to me, and I correct the omission, but I forgot it's in a public enum. So that's a semver break.
Why? If somebody writes code using FederalHoliday from my library, they're entitled to exhaustively match against the enumerated type. So they can make code to handle MLK Day and Thanksgiving and so on, and of course they had no reason to handle Juneteenth because it wasn't in the list - then one day they take my update. Their program no longer builds, it complains that their FederalHoliday code isn't handling Juneteenth, I made a semver breaking change.
Now, in this type of case the correct fix (which you need to do before release, as introducing it later is a semver break) is to mark the enumeration as #[non_exhaustive] which tells Rust I know what's in this enumeration, so I can use it as an exhaustive match in my own code, but outside this crate, you can't match it exhaustively, you need to write a default case to handle new additions.
> it's an example of IFNDR, a get-out in which the language standard just washes its hands of difficult problems by saying in that case what you've written is silently not a C++ program, so its behaviour is arbitrary but you don't get any compiler diagnostics
UB is runtime behaviour, which is why "UB? In my lexer?" (the proposal paper for C++ 26 to remove Undefined Behaviour from the lexer, which clearly isn't happening at runtime) is so hilarious - C++ is so badly defined even parsing a source file has undefined behaviour, LOL.
Because it is runtime behaviour UB is constrained, if the only UB is in your "Delete files" code, and that code only executes when the user hits the "Delete" button, we can say that this UB won't occur when the user doesn't hit the button.
But IFNDR isn't like that, since the entire program has no meaning, nothing about it is required at all. Maybe the compiler output is 40GB of zeroes. Maybe when you run the program it immediately segfaults. Maybe it works exactly as you expected... except it's a bit... off and you can't exactly say why.
So IFNDR is a property of programs, whereas UB is a property of executions of programs. Got it, thanks.
Given that IFNDR is a static, rather than a dynamic property, why can't compilers detect it and error? Are all IFNDR issues similar to the ODR one you mentioned in that they only arise at link time and fixing them would require changing how symbols are mangled?
> Are all IFNDR issues similar to the ODR one you mentioned in that they only arise at link time and fixing them would require changing how symbols are mangled?
No, they're worse. Are you comfortable with basic CS theory? If not this is likely to whizz over your head, sorry.
A typical IFNDR issue depends upon the semantic properties of the alleged C++ program. Rice's Theorem tells us that all non-trivial semantic properties are Undecidable. No algorithm can exist to determine whether a program has these properties except if trivially all the programs in your language have the property (e.g. imagine a toy language with no jumping or iterating features, it must halt) or none do (e.g. imagine a resolutely single threaded language, it doesn't have any data races).
Because it's Undecidable, having the compiler decide if your alleged C++ program has the desired properties and if not report an error is intractable. So the C++ standard just says if it doesn't have that property it's actually not a C++ program, too bad, the compiler shouldn't worry about this case at all, just press on.
Now, the good news about Rice's Theorem is that we do have another option. When we're not sure if the semantic property we wanted is present, we reject the program, with a diagnostic informing the programmer that we couldn't prove it had this desirable property.
I'd argue this approach is much better, but it does mean you can write programs in Rust (which has this behaviour) where even though you are totally correct that the program would actually be OK the compiler rejects your program because it can't see why you're right.
In which case static or dynamic linker will simply fail because Rust symbols have hashes derived from ABI attached by default (unless `#[no_mangle]` is used and you're on your own) and those hashes won't match unless the identical signature was used.
You have a type T, and a struct U which contains a T.
A function takes a U by reference, and therefore will be mangled with U.
However say that function was built with a different version of T than the caller. This doesn't affect mangling. Things silently work even though they break.
This is a basic pitfall of working with any system that allows linking of binaries.
The setup only works so long as you guarantee that all entities with the same name are indeed the same thing. This is the One Definition Rule.
Those hashes are exactly designed in the way that any such changes will alter hashes and thus stop linkage. Virtually all C++ name mangling schemes don't even consider such cases: `void foo(T&);` will probably result in the same mangled name even though T's layout or any type referenced by T has been changed. In Rust they do affect mangling.
Rust doesn't have this get-out. To some extent that's because it's not a formal standard, but mostly it's because IFNDR is diametrically opposed to correctness. Many C++ programs (perhaps even the vast majority) technically have no meaning whatsoever due to IFNDR which is bad -- it's not that the C++ compiler judges them to be correct when they aren't, but that it has no opinion either way, ever and programmers mistake "it compiled" for success.
Thus, in Rust you don't get "ODR errors" here, your program won't compile.
Here's a nice easy Rust semver break example: I make a library about US vacation booking, it has a public enumerated type FederalHoliday but initially that enum doesn't mention Juneteenth - whoops. Somebody points that out to me, and I correct the omission, but I forgot it's in a public enum. So that's a semver break.
Why? If somebody writes code using FederalHoliday from my library, they're entitled to exhaustively match against the enumerated type. So they can make code to handle MLK Day and Thanksgiving and so on, and of course they had no reason to handle Juneteenth because it wasn't in the list - then one day they take my update. Their program no longer builds, it complains that their FederalHoliday code isn't handling Juneteenth, I made a semver breaking change.
Now, in this type of case the correct fix (which you need to do before release, as introducing it later is a semver break) is to mark the enumeration as #[non_exhaustive] which tells Rust I know what's in this enumeration, so I can use it as an exhaustive match in my own code, but outside this crate, you can't match it exhaustively, you need to write a default case to handle new additions.