Suppose I have a complex project P that uses libraries A and B, and these libraries use header-only library T containing some templates.
P --> A --> T(1.0)
|
---> B --> T(1.0)
The authors of T release version 1.1, which is backward compatible at the source level, i.e. any user of T can be recompiled with the new version, and the program will continue to work. Suppose A decides to upgrade, but B stays on T version 1.0.
P --> A --> T(1.1)
|
---> B --> T(1.0)
Am I correct in assuming that virtually any change to T would be a violation of the ODR rule, as stated here: https://en.cppreference.com/w/cpp/language/definition.
There can be more than one definition... as long as all of the following is true: each definition consists of the same sequence of tokens (typically, appears in the same header file)
Practically speaking, are there any technically illegal, but practically "safe" changes?
The standard considers any lexical change inside the definition (in terms of tokens after preprocessing) to violate the one definition rule. There are no exceptions. And this is also only the base rule. There are further requirements e.g. that all corresponding names in the definitions will refer to the same entity in each definition (after overload resolution and partial template specialization matching).
So, from that point of view, you always have to recompile every translation unit in your program (whether in a library or not) that includes the header with the modified definition.
Of course that's usually not how it is done in practice.
In practice one can generally rely on the ABI specification, or at least common patterns in ABI specifications, as well as general knowledge how compilers work or how linking on the specific platforms of interest works. With that knowledge it is often possible to derive that certain changes will not have any unintended consequences even if the header file is mixed with an old version between translation units.
Unfortunately there is no easy rule to follow. It will depend on what exactly you intent to change.
For a simple example: Adding a non-virtual non-special function with a new name as a class member will generally be fine. The way compilers and ABIs work, nothing at the ABI level will change by doing this and because the name is new there is no risk of overload resolution resulting in indirect ODR violations if e.g. one inline function definition happens to use the old overload resolution result and another definition for the same function happens to use the new one.
One way to avoid at least the direct ODR issues completely is to wrap the whole header-only library in an unnamed namespace, causing everything in it (including classes and templates) to have internal linkage, i.e. they will simply be different entities in every translation unit.
Of course, that's usually not what one wants because it makes sharing the contents between translation units impossible.
Also, it actually increases the risk of indirect ODR violations, because any inline definition with external linkage must then not depend on the contents of the header-only library.