Why program using inline functions have different behaviors according to link order and parameters?

126 views Asked by At

I know the answer to this but we can have some fun analyzing it. And we'll learn having fun!

I've used gcc 4.1.2 for those tests.

First of all, this code is not standard, since an inline function will have different definitions in different translation units. I know that. But let's analyze what's going on and give the answer to three questions I'll make. We'll learn from it :)

I'll keep files simple (no #ifndef guards for example).

Suppose I have those files:

increment.h:

inline int increment()
{
    static int value = 0;
    return ++value;
}

decrement.h:

int decrement();

decrement.cpp:

inline int increment()
{
    static int value = 0;
    return --value; // Attention to this
}

int decrement()
{
    return increment();
}

main.cpp:

#include <iostream>
#include "increment.h"
#include "decrement.h"

using namespace std;

int main()
{
    cout << increment() << endl;
    cout << increment() << endl;
    cout << decrement() << endl;
}

If I compile them with this Makefile:

CC=gcc
CFLAGS=-I. -O2

crazy: main.o decrement.o
        $(CC) -lstdc++ main.o decrement.o -o crazy

main.o: main.cpp increment.h decrement.h
        $(CC) $(CFLAGS) -c main.cpp -o main.o

decrement.o: decrement.cpp decrement.h
        $(CC) $(CFLAGS) -c decrement.cpp -o decrement.o

clean:
        rm -f *.o *.~ crazy

The output is:

1
2
1

If I remove the -O2 flag from the Makefile:

  CFLAGS=-I.

The output is:

1
2
3

If I also change the order of main.o and decrement.o (leaving it without the -O2 flag as I just did):

$(CC) -lstdc++ decrement.o main.o -o crazy

the result is:

-1
-2
-3

What is going on here? Why the -O2 flag and the order of the object files linking change the output this way?

1

There are 1 answers

0
Daniel Munoz On

There are two translation units here: main.cpp and decrement.cpp

Lets replace our #include directives for increment.h and decrement.h to see how main.cpp and decrement.cpp look like after the preprocessor pass (I won't replace other includes):

decrement.cpp:

inline int increment()
{
    static int value = 0;
    return --value; // Attention to this
}

int decrement()
{
    return increment();
}

main.cpp:

#include <iostream>

inline int increment()
{
    static int value = 0;
    return ++value;
}

int decrement();

using namespace std;

int main()
{
    cout << increment() << endl;
    cout << increment() << endl;
    cout << decrement() << endl;
}

In the case of decrement.cpp it exports symbols similar to those to the linker:

(inline) int increment::value = 0;
inline int increment(); // exported only if -O2 flag is NOT specified, with -O2 it is inlined
int decrement();

In the case of main.cpp, it exports:

int (inline) increment::value = 0;
inline int increment(); // exported only if -O2 flag is NOT specified, with -O2 it is inlined
int main();

If increment is actually inlined (-O2 flag set), "int::increment value = 0" gets defined somewhere by the linker and those translation units become:

decrement.cpp

int decrement()
{
    return --increment::value;
}

main.cpp

#include <iostream>

int decrement();

using namespace std;

int main()
{
    cout << ++increment::value << endl;
    cout << ++increment::value << endl;
    cout << decrement() << endl;
}

So we get this behavior:

1
2
1

But if we remove the -O2 flag, the compiler doesn't inline the functions, but it creates a body for them instead (so it will be easier to debug, since the debugger will have a unique place to put a breakpoint for that function). Therefore two bodies will be created for "inline int increment()". One in decrement.cpp (decrement.o), which will decrement the value, another in main.cpp (main.o), which will increment the value.

At link time, if "int increment()" were not inline, multiple body definitions would lead to linker error since the body of a function should be defined only once. But since it is inline, the linker supposes that all bodies for "inline int increment()" are the same throughout all translation units, since the standard states that all inline functions must have the same body everywhere, the linker just picks the first of them.

If you link:

$(CC) -lstdc++ main.o decrement.o -o crazy

The first body is the one in main.o, which increments value. So you get:

1
2
3

But if you link:

    $(CC) -lstdc++ decrement.o main.o -o crazy

The linker picks ther body from decrement.o, which actually decrements value. So you get:

-1
-2
-3

Question: What could happen if one body (main.o) initializes value to 0 and the other (decrement.o) initializes it to, for example, value = 3? What do you think?