Playing a game of chance with C++ inline keyword

One day I broke a weekly CI build by optimizing an innocent looking class method. Or, to be more precise, I broke the build that ran a week after the optimization. Or, to be more honest, I broke the code half a year before I broke the CI build. Ok, it gets complicated. I'll try to explain it step by step.

You see, in C++ inline keyword doesn't mean function inlining as it does in C. There is a widespread opinion that it works like an advice to a compiler, but this is, of course, a misconception. Compilers don't want advice from us — meat-made people.

What it does in reality — it releases a compiler from the obligation to add an actual callable function in a translation unit. It might be there on certain occasions, but it might as well be missing.

Half a year before

There was a class with a header file and a cpp file. Some of the methods were defined in the header file as inlinable.

// in SomeFile.h inline TOutput SomeClass::SomeMethod(TInput input) { auto output = DoLotOfThings(input); return output; }

It's not nice to expose code like this for no particular reason. Since the operation itself was heavy, it couldn't be inlined anyway, and even if it could, the penalty of the actual call would be too small relative to the body of the method to consider.

So I moved it to the cpp file and made sure there is a __declspec(dllexport) before the declaration in the header file.

// in SomeFile.h __declspec(dllexport) TOutput SomeClass::SomeMethod(TInput input); // in SomeFile.cpp inline TOutput SomeClass::SomeMethod(TInput input) { auto output = DoLotOfThings(input); return output; }

My mistake here — I forgot to remove the inline. But it worked fine on my machine, it passed the gated build, and it was passing all the weekly CI builds in every possible configuration.

The day X

I found a way to reduce DoLotOfThings to DoFewThings. Hurrah!

I did the changes in the code.

// in SomeFile.h __declspec(dllexport) TOutput SomeClass::SomeMethod(TInput input); // in SomeFile.cpp inline TOutput SomeClass::SomeMethod(TInput input) { auto output = DoFewThings(input); return output; }

I ran a local test build. Then I pushed it through the gated build. Then I waited to see if the night builds were ok. They were ok. They were all ok.

The week after

We have to support quite a lot of configurations and for a large project. And we have quite a lot of tests too. The gated check-in triggers builds for a few basic configurations. Then the nightly tests trigger a few more every night. Then finally once a week all of the configurations are built and tested. Now let's say my optimization was enough for MSVC to make code inlinable. And let's say not for Clang. Now in Debug configuration inlines are turned off by default, and in Release they are on. Also in Dynamic build dllexport plays its role making the compiler produce things that are promised in DLL interface, but in Static it doesn't.

So there is one particular configuration for which the compiler doesn't have to make SomeClass::SomeMethod as a separate function. But something in another translation module tries to call it, and this makes a linker a bit upset. So the build fails unexpectedly a week after the optimization has been checked in.

After the local testing, after the gate, and after the nightly tests.

Conclusion

C++ inline is more of a rudiment than a tool. Most of the time compiler does a great job inlining things anyway. It doesn't need a special word for that.

Also, most of the time you wouldn't shoot yourself in a foot immediately by putting inlines here and there, so people do that. But you don't have to shoot yourself to get hurt. Planting a landmine works just like that. Only more unpredictably. And you definitely can do this with inlines.

To give you an impression of what it feels to have unpredictable bugs, I made a build configuration slot-machine. Enjoy!

Pull the lever!