Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Despite violation of the One Definition Rule, how is it possible that a compiler/linker COULD choose an alternate inline constructor?

Referring to What determines which class definition is included for identically-named classes in two source files?, in which there is a deliberate, clear violation of the One Definition Rule, I am still confused how it is even POSSIBLE for the compiler/linker to have the option of selecting one definition over another.

(ADDENDUM based on answers/comments: I am looking for a single example of how a compiler/linker could produce the result indicated below, given code that is deliberately in violation of the standard and that therefore the code results in undefined behavior.)

The code sample is:

// file1.cpp:

#include <iostream>
#include "file2.h"

struct A
{
    A() : a(1) {}
    int a;
};

int main()
{
    // foo() <-- uncomment this line to draw in file2.cpp's use of class A

    A a; // <-- Which version of class A is chosen by the linker?
    std::cout << a.a << std::endl; // <-- Is "1" or "2" output?
}

...

//file2.h:

void foo();

...

// file2.cpp:

#include <iostream>
#include "file2.h"

struct A
{
    A() : a(2) {}
    int a;
};

void foo()
{
    A a; // <-- Which version of class A is chosen by the linker?
    std::cout << a.a << std::endl; // <-- Is "1" or "2" output?
}

In this case, the function foo() sometimes prints 1, and sometimes 2.

But the constructor for A is inline! It's not a function call! Therefore, I would think that the compiler must include the assembly/machine instructions for the code that instantiates the object a within the compiled code for function foo() itself at the time that the function foo() is compiled.

Therefore, I would think that later, at linking time, the linker will NOT change the assembly/machine instructions for the definition of foo() when it decides to include the function foo() in the compiled binary (since it's only known that foo() is, in fact, being called, at linking time). According to this reasoning, the linker could not possibly influence which inline constructor code is compiled into the function foo(), so it must be file2's version of the inline constructor that is always used, despite the deliberate violation of the One Definition Rule.

If the constructor for A were NOT inline, then I would understand that when the function foo() is compiled, a JUMP statement to a function (the constructor for A) might be placed inside the assembled code for the function foo(); then, later, at linkage time, the linker could then fill in the address of the JUMP statement with its choice of the two definitions of the constructor for A.

The only explanation I can think of for the fact that in reality, sometime foo() prints 1 and sometimes foo() prints 2 despite the presence of the inline constructor is that the compiler, when it compiles "file2.cpp", creates SPACE in the compiled assembly/machine code representing the function foo() for the inline call to the constructor for A, but does not actually fill in the assembly/machine code itself; and that later, at linkage time, the linker copies the code for the constructor for A into the pre-determined location within the compiled definition of the function foo() itself, using its (arbitrary) choice between the two definitions of the inline function for the constructor for A.

Is my explanation correct, or is there another explanation? How is it possible, in this example, despite the deliberate violation of the One Definition Rule, for the compiler/linker to have a choice in which constructor for A is called, given that the constructor call is inline?

ADDENDUM: I changed the title and added a paragraph of clarification near the top, in response to comments and answers, to make it clear that I understand that the behavior is undefined in this example, and that I am looking for a single example of how a real compiler/linker could produce the observed behavior even once. Note that I'm not looking for an answer that predicts what the behavior will be at any particular time.

ADDENDUM 2: In response to a comment, I have placed a breakpoint at the line A a; in the VS debugger and selected the "disassembly" view. Indeed, it is plain as day from the disassembly code that DESPITE the presence of "inline", in this case the compiler has chosen NOT to inline the constructor call for the object a:

Disassembly view of the line `A a; from the code sample in the question: The compiler has chosen not to inline the constructor call, despite its being inline.

Therefore, Alf's answer is correct: Despite the implicit inline of the constructor, the constructor call has NOT been inlined.

A tangential question therefore arises: Can a clear-cut statement be made - one way or the other - regarding whether constructors are less likely to be inlined than regular member functions (assuming inline is present, either explicitly or implicitly, in both cases)? If a statement can be made about this and the answer is "yes, the compiler is more likely to reject inline for a constructor, than it is to reject inline for a regular member function", then a follow-up question would be "why"?

like image 311
Dan Nissenbaum Avatar asked Dec 16 '22 16:12

Dan Nissenbaum


1 Answers

Defining your constructor in the class definition is equivalent to using the keyword inline and an out-of-class defintion of it.

inline does not require/guarantee inline expansion of machine code. It hints about that, but that's all.

The guaranteed effect of inline is to allow the same definition of the function in multiple translation units (it must then be defined, essentially identically, in all translation units where it's used).

Thus, your logic based on assumption of required/guaranteed inline expansion of calls, yields incorrect conclusions due to incorrect assumption.

like image 158
Cheers and hth. - Alf Avatar answered May 26 '23 20:05

Cheers and hth. - Alf