📜 ⬆️ ⬇️

One Definition Rule, inline and unexpected consequences of their combination

C ++ requires that any function be defined no more than once - One Definition Rule, ODR. As soon as you define a function with the same name and signature in different translation units (.cpp files), you receive an error indication at the linking stage.

inline functions are usually defined in header files (.h) so that all translation units can see the implementation of the function and substitute it at the call site. Accordingly, as soon as you include a header file with such a function in more than one translation unit, the ODR will be formally broken, but ... you will not receive any error indication.

Why and what unexpected consequences could this have?
Why is the question relatively well-known ( for example ). On the one hand, it is impossible to prohibit the situation described above for practical reasons - it is necessary that the implementation of the function be available from all translation units causing it, otherwise substitution may be impossible. On the other hand, the ODR is broken and it would be good to respond to it.
')
You can respond in two ways - by an error message or by silence. In this particular case, the linker selects the second. As soon as the linker sees more than one inline function with the same name and the same signature, it considers that it is the same function , and selects one of them at its discretion .

So a formal violation of ODR is formally eliminated.

Suddenly, there is a wide scope for difficult-to-detect defects. The policy used by the linker assumes that the function is really the same, i.e. identical implemented. We are waiting for an example - stock up on chips and read on.

Slowly stirring, add a little preprocessor (for example, this is done for the error handler in ATL ):

 //CommonFile.h __declspec( noinline ) //  noinline inline void HandleErrorCondition( int condition ) { #ifndef OVERRIDE_STANDARD_HANDLING _exit(1); #else CustomHandleErrorCondition( condition ); #endif } //StaticLib.h #include <CommonFile.h> inline void SomeUsefulFunction() { //blahblahblah HandleErrorCondition( 0 ); } //StaticLib.cpp #include <StaticLib.h> blahblahblah,  SomeUsefulFunction() //Executable.cpp void CustomHandleErrorCondition( int condition ) { throw MyCustomException( condition ); } #define OVERRIDE_STANDARD_HANDLING #include <StaticLib.h> blahblahblah,  SomeUsefulFunction() //V2UncmUgaGlyaW5nIC0gd3d3LmFiYnl5LnJ1L3ZhY2FuY3k= 

StaticLib.cpp is compiled into the StaticLib.lib static library, then Executable.cpp is compiled into an executable file (.exe or .dll is all the same) and statically links StaticLib.lib.

The HandleErrorCondition () signature contains __declspec (noinline), a Visual C ++ attribute that tells the compiler that you never need to substitute the implementation of this function. This is done specifically so that the compiler does not substitute the implementation of the function and the implementation can be replaced later. Visual C ++ obeys.

The cunning plan ™ for which this kitchen is needed is obvious: if the developer is satisfied, the default handler will be used. If the default handler is not satisfied, you can install your own.

Will this work? Which implementation of HandleErrorCondition () - with a _exit () call or with a CustomHandleErrorCondition () call - will be called?

Unknown.

When the compiler compiles StaticLib.cpp, it includes the first implementation in the object file (StaticLib.obj) with the _exit () call. When the compiler compiles Executable.cpp, it includes in the object file (Executable.obj) a second implementation with a call to CustomHandleErrorCondition ().

When linking, the situation described above with violation of ODR occurs, but now two inline functions that are identical from the point of view of the policy used by the linker have different implementations. The linker will choose some one and not the fact that the choice will not change from one link to another.

Suddenly your program does not work as you planned. What is especially nice is that the behavior in this example will differ only in error handling, i.e. in relatively rare situations and not the fact that they will not forget to check.

The described behavior (the choice of one of the functions) is demonstrated by Visual C ++. Some readers are already preparing to write a caustic comment, but in vain. In accordance with standard C ++ ISO / IEC 14882: 2003 (E), paragraph 3.2 / 5, in the described situation, the behavior is undefined. Accordingly, the linker is not obliged to give any reasonable results, nor to give the same results when re-linking the same object files. In some cases, the behavior will be what you expected, in some, perhaps not. So Visual C ++ is innocent.

It is time to note that the example looks rather artificial and curved. Again, __declspec (noinline) is a feature specific to Visual C ++. There are lots of other ways to be exactly in the same situation.

For example, in two different headers there may randomly be different inline functions with the same signature. If there is no situation where both headers are included in the same .cpp file, you will not get a compilation error and you will find yourself in a situation of breaking the ODR. Still different .cpp files can be compiled with different values ​​of any compiler settings that change the behavior of the code. Again the same situation. #pragma pack can also make a contribution.

Finally, if __declspec (noinline) is missing, in some cases the compiler may substitute the implementation of the function corresponding to the settings of the compiler and the preprocessor symbols specified for the corresponding translation unit.

The scope for defects in this direction is endless. Detecting such defects when they are already in your code is extremely difficult. The behavior of the program can change during perelinkovka, and can remain the same - respectively, the defect can be difficult to even reproduce.

The solution is one - if your code uses inline functions, make sure that the compilation of different translation units is performed in such a way that the behavior of these functions remains unchanged. The compilation settings must be the same, the preprocessor characters must be the same.

In the case described above, you need to define OVERRIDE_STANDARD_HANDLING in the StaticLib project and rebuild it.

Take care of yourself.

Dmitry Mescheryakov
Department of Data Entry Products

Source: https://habr.com/ru/post/108166/


All Articles