Language Shenanigans

Table of Content

Force Inline in C++

This is a secondary page preview. As evident, content is displayed on this type of page.

Force Inline in C++

There is a tiny lie hiding in C++.

It is spelled like this:

inline

When you first meet it, the name seems friendly enough. "Inline." Surely this means put the body of this function directly where I call it. That is what the word means in English, and C++ only occasionally enjoys punishing us for knowing English.

But that is not really what inline means. Not anymore.

In modern C++, inline is mostly about linkage — it lets you define a function in a header without angering the linker gods when that header gets included from multiple translation units. It may also nudge the compiler toward inlining, but the compiler was probably considering that already.

So when we write:

inline int decrement(int value)
{
    return value - 1;
}

we have not commanded anything. We have made a suggestion. A polite one. The compiler may smile, nod, and then do whatever it was going to do anyway.

Most of the time, that is fine. Compilers are not toddlers. They are complicated pieces of software written by people who spend a lot of time thinking about instruction caches and register pressure. If a compiler decides not to inline something, it usually has a reason.

"Usually" is not "always," though, and "probably" is a dangerous word to build a library on.

The Little Function That Should Disappear

Look at this:

int decrement(int value)
{
    return value - 1;
}

There is almost nothing here. A value comes in, one gets subtracted, the result goes out. If this function call survives into the final machine code, something has gone sideways.

Now look at this:

int factorial(int value)
{
    return value == 0 ? 1 : value * factorial(value - 1);
}

It looks small too, but it has a trapdoor in the floor. It calls itself. Inlining a recursive function means unrolling a staircase without knowing which floor it ends on. The compiler might manage it in constrained cases, but it cannot promise you anything.

So here is what we want. This should compile:

FORCE_INLINE int decrement(int value)
{
    return value - 1;
}

And this should fail:

FORCE_INLINE int factorial(int value)
{
    return value == 0 ? 1 : value * factorial(value - 1);
}

Not quietly. Not with "well, I tried." We want the build to stop and tell us.

That distinction matters. We already have "please inline this if you feel like it." We want "inline this, or admit you cannot."

Why Would We Want This?

If you are writing an application, the usual answer is boring and correct: profile it, fix what matters, go make lunch.

But libraries live in a stranger world.

When you write a library, you do not know where your code lands. Your tiny wrapper function might sit in a cold path no one cares about, or it might get called billions of times in someone's physics solver. You do not know. So library authors build small abstractions — a handle type instead of a raw integer, a checked index instead of a naked pointer, a wrapper around a CPU intrinsic — and the deal those abstractions make with their users is: you get the safer interface, and it costs you nothing.

For that deal to hold, the abstraction has to disappear after compilation.

Let's build the spell that makes that happen.

One Macro, Three Compilers

The three big compiler families each have their own way of saying "I mean it."

#if defined(__clang__)

#define FORCE_INLINE [[gnu::always_inline]] [[gnu::gnu_inline]] extern inline

#elif defined(__GNUC__)

#define FORCE_INLINE [[gnu::always_inline]] inline

#elif defined(_MSC_VER)

#pragma warning(error: 4714)
#define FORCE_INLINE

This is not beautiful. Compiler abstraction macros rarely are. They are the plumbing behind the wall — we do not frame them and hang them up, but we are glad the sink drains.

GCC gives us always_inline, and it has teeth. If GCC cannot inline the function, it produces an error. Good. GCC understood the assignment.

Clang is slippier. It supports always_inline but sometimes treats it as a very stern suggestion rather than a hard requirement. So we add [[gnu::gnu_inline]] extern inline alongside it. The trick: we arrange things so Clang never emits an out-of-line copy of the function. If inlining succeeds, great. If it does not, the linker goes looking for a function body and finds only tumbleweeds — and gives us a linker error.

Is that as clean as a compiler diagnostic? No. Is it effective? Yes. File this one with the sharp screwdrivers: useful, but not something to wave around carelessly.

MSVC gives us __forceinline, but it reports failure as warning C4714. A warning is not enough. So we promote it:

#pragma warning(error: 4714)
#define FORCE_INLINE

One catch: MSVC only emits that warning when optimization is enabled (/O1 or /O2). That makes sense — asking serious questions about inlining in a debug build is like asking how aerodynamic your parked car is.

Do Not Sprinkle This Everywhere

Now the parental warning, and it matters.

FORCE_INLINE is not seasoning. Do not put it on every function because "function calls are slow." That sentence has launched a thousand bad codebases. Inlining makes binaries larger, can hurt instruction cache behavior, and can — genuinely — make performance worse. That last one feels rude, after all this trouble.

Good candidates look like this:

FORCE_INLINE std::uint32_t index() const { return index_; }

FORCE_INLINE bool is_valid(Entity e) { return e.id != invalid_entity; }

These are not algorithms. They are vocabulary — tiny pieces of structure that exist to make calling code nicer and safer. They should vanish.

Bad candidates look like this:

FORCE_INLINE void update_world(World& world, float dt);

FORCE_INLINE virtual void draw()

A virtual call is resolved at runtime. There is no attribute for "please make my design cheaper."

Why Not Just Use a Macro?

You might wonder why we bother. Why not write:

#define DECREMENT(x)

It works. In the same way a raccoon can technically open a trash can — there is intelligence there, but you may not love the result.

Macros do not respect scope. They do not type-check. They can evaluate arguments twice. They show up weirdly in debuggers. They are textual substitution wearing a fake mustache.

A function with FORCE_INLINE gives us a real name, a real type, a real body — and the compiler still turns it into the same machine code. We just did not have to invite the preprocessor into the room to get there.

That is the whole point. We are not forcing inline because we hate functions. We are forcing inline because we like functions and want to use them where people usually reach for macros.

The Design Principle

The healthiest reason to reach for FORCE_INLINE is not "make this faster."

It is this:

I am building a zero-cost abstraction, and I want the build to fail if it stops being zero-cost.

That framing keeps the tool in its place. It belongs at library boundaries, around typed wrappers and low-level utilities. Not scattered everywhere because we got nervous about call overhead.

Most of the time, trust the compiler.

Sometimes, make it prove you right.

Get Template for free

Get Template for free

Create a free website with Framer, the website builder loved by startups, designers and agencies.