Skip to content
Why is a raven like a writing desk?

Thoughts both confusing and enlightening.

Why is a raven like a writing desk?

Thoughts both confusing and enlightening.

The Global API Injection Pattern

elbeno, 12 April, 2026

I’ve mentioned this in a few conference talks now, and periodically people ask me about it, but until now I guess I haven’t written it up.

It starts with a common, dare I say ubiquitous, problem: how to test code that uses some “global” API. Could be for example file IO, network IO, allocation, logging, concurrency-related, etc. In general this API is often going to be procedural and based on functions rather than object-oriented, but that’s not a hard-and-fast rule; it just makes for easier examples.

So let’s take a simple example: a putative one-function logging API.

auto log(auto&&...) -> void;

Presumably, the arguments to log are a format string and some values to be formatted, and there is no return value here. The workings of log are not the important part here. And this function could be wrapped by a macro, or whatever. I’m aware that in real life, logging can get arbitrarily complex… and that arbitrary complexity can still live under this, but we’re not interested in the actual behaviour here so much as how we abstract it.

Of course, if the question is, “how do I test X?” in general we know the answer: “use dependency injection.” So that’s what this pattern does. There are lots of ways to do DI; historically I’ve done a bunch of them.

One C-style approach is to solve this at link time: production code links one log implementation, test code links another. It’s workable; I’ve done that on projects in the past. An alternative, similar approach is to solve this with conditional compilation. It amounts to the same thing, just putting the code directly into the application instead of into a separate library.

An object-oriented approach would be to turn our log function into a logger class with a virtual log function. Maybe provide access through a singleton. Maybe pass a logger object in at the top of the object/call stack, and thread it through to where it needs to be.

There are many variations here, but I’ve seldom been happy with using any of them. In particular the object-oriented solution is incredibly common in my experience; test frameworks just love mocks, and encourage that kind of thing. But the drawbacks are numerous:

  • Introducing virtual functions adds a virtual call overhead. Maybe not a problem for some APIs where it would be dwarfed by IO overhead, or perhaps for some platforms where the overhead is negligible. But it is noticeable in some cases and we care about performance: what if you’re on an embedded platform which can’t make virtual call overhead disappear? What if you want logs to be incredibly cheap so that they can be everywhere?
  • Accessing the API tends to be problematic. If you make a singleton, now you have to manage it, and you’ve introduced a setup/teardown lifetime dependency where perhaps it didn’t need to exist. If you pass in an API, you tend to get the Passthrough Antipattern, aka carried dependencies, where branch classes/functions that don’t care about and don’t use the API have to receive it in order to pass it on to their leaf children which do need it. The result is bloated function and constructor parameter lists.
  • The assumption that there’s only one of these APIs also tends to be problematic over time. A major problem with “singletons” (of whatever stripe) is that they don’t stay singular. You think you only have one of these — maybe it’s in hardware! — and then suddenly you need another. You need a secure log path as well as a regular one. So maybe you code up the the dichotomy and live with it; a kind of architectural “rule of 3” (start with one; duplicate and distinguish for two; generalize when you need a third). But in the end, premature pluralisation is seldom a mistake.

Well OK, enough problem statement. I’m sure that if you’re a working programmer, you have seen plenty of this kind of code. So on to how we’re going to solve it a bit differently.

We take our global API…

auto log(auto&&...) -> void;

…and we put it inside a class.

struct logger {
  static auto log(auto&&...) -> void;
};

It doesn’t have to be a static function, but it probably can be. Fine. Next, we make a variable template, and turn the original global function into a function template:

template <typename...>
constexpr inline auto log_api = logger{};

template <typename... DummyArgs, typename... Args>
auto log(Args&& ...args) -> void {
  auto& api = log_api<DummyArgs...>;
  api.log(std::forward<Args>(args)...);
}

Finally, in our application, we specialize the variable template for the logger that we want:

template <>
constexpr inline auto log_api<> = my_logger{};

When we make a call, the right thing happens.

log("Hello, {}!", "world");

The DummyArgs... pack is deduced to be empty. (And if you want to be extra safe, you can add a constraint on the function template to make sure that’s the case and prevent accidental explicit template arguments.) Nevertheless it is used inside the log function template to select the correct specialization, and dispatch the call to the correct place.

Squinting, we’re using templates to achieve a kind of static formulation of the object-oriented approach. The template specialization is the equivalent of global access through a singleton. Some benefits of this approach:

  • Multiple log implementations can live in the code, waiting to be selected, without conditional compilation. They are different structs that implement the API differently. Only one will be selected by the specialization. The rest melt away at link time, unused.
  • The default implementation can be the null logger. This is great for reducing test boilerplate, because most tests aren’t testing logging, don’t care about logging, don’t need to set up mocks for logging, etc. If the variable template is left unspecialized, the primary template will be used. Tests are quiet by default.
  • The default implementation can optionally protect against accidents. If we want it to be an error not to specify an API implementation, the default can static_assert inside its function(s).
  • Library code (or subcomponents in application code, which is really saying the same thing) doesn’t need to be passed the logging API: it just uses log.

And some answers to questions about potential problems with the approach:

Q. Aren’t global variables bad?
A. Global variables that have mutable state are problematic. Stateless global variables that represent global APIs are fine, in my opinion. Global access is just a fact of life sometimes.

Q. You mentioned plurality. This still looks like a singleton to me?
A. Plurality can be achieved by adding template arguments to our specializations.

struct secure;
struct insecure;

template <>
constexpr inline auto log_api<secure> = my_secure_logger{};

template <>
constexpr inline auto log_api<insecure> = my_insecure_logger{};

log<insecure>("Hello, {}!", "world");
log<secure>("(which I have a secret plan to take over)");

Q. Does this require that my implementation is header-only?
A. No. It requires that the “selection” function (log) is in a header, but the actual logging implementation can be in a separate translation unit.

Q. Isn’t this an ODR violation farm?
A. Well… it depends. Fair warning: you do need to be aware of the ODR issue, and this is why I position it for use specifically with global APIs which would likely otherwise be singletons. Every translation unit that uses log needs to see the same variable template specialization. We can’t have different logging implementations used in different translation units, seeing different variable template specializations. This is also a level of freedom that this pattern gives up that the virtual function-based API approach retains.

On the other hand, if you don’t need that runtime freedom (which for many uses we don’t), and if the API is global (which for these uses it is), you’d be doing an equivalent thing anyway: including some global header to provide the access. It’s much the thing same here.

Corollary: you want your test executables to be easy to proliferate and probably single-translation-unit. If you have tests that want to test logging output, it should be easy to spin up a new test executable that specializes the logger with an implementation that goes to a string that tests can inspect.

We can think of this in the same vein as specializing std::hash for using our own types as keys in a std::unordered_map. Because it really is the same mechanism; we’re just using it to provide a global API, likely used across the codebase, instead of limited to a few translation units. All the same ODR concerns apply.

A final observation on an important part of the pattern:

template <typename... DummyArgs, typename... Args>
auto log(Args&& ...args) -> void {
  auto& api = log_api<DummyArgs...>;
  api.log(std::forward<Args>(args)...);
}

The return type here is important. It must be known; it cannot depend on the specialization of log_api. This is kind of the “secret sauce” here that means the compiler doesn’t have to instantiate this function template to figure out what it returns. In turn, that means that the compiler doesn’t need to instantiate the log_api specialization before it is provided. Which is the key to how this pattern works.

As long as we can write a concrete return type here that doesn’t depend on the log_api specialization, it’s all good. And for the kind of global, well-known API shapes this pattern can typically help with, that’s almost always the case.

By the way, the return type can still depend on the arguments passed to the function – that’s absolutely fine. For instance, if we passed a lambda to the function, we could return what it returns. No problem there — so we can use this pattern for things like…

template <typename... DummyArgs, std::invocable F>
auto call_in_critical_section(F &&f) -> std::invoke_result_t<F> {
    auto &p = conc_policy<DummyArgs...>;
    return p.call_in_critical_section(std::forward<F>(f));
}

…where we provide and specialize a concurrency policy appropriately for the platform. Perhaps we turn off interrupts on embedded hardware, and use a regular std::mutex in desktop tests. This means we can take our platform code and subject it to thread sanitizer in tests, which is a big win.

So that’s what I’ve been referring to as the global API injection pattern. An alternative to other forms of dependency injection that has a lot of nice properties. Particularly suited to using global, cross-cutting APIs, with (as always) an appropriate modicum of understanding. In my experience it really reduces the amount of mock boilerplate in tests. To be clear, it’s not a panacea, and you still have to structure the code so that it’s testable and does DI in the first place. But I’ve found it’s a really good option for the actual mechanism of DI.

C++

Post navigation

Previous post

Related Posts

Recursive lambdas

16 April, 201530 June, 2015

One can assign a lambda to auto or to std::function. Normally one would assign a lambda to auto to avoid possible unwanted allocation from std::function. But if you want recursion, you need to be able to refer to the lambda variable inside the lambda, and you can’t do that if…

Read More

Exercising Ranges (part 7)

1 July, 20151 July, 2015

(Start at the beginning of the series if you want more context.) Calculus operations It’s relatively simple to do differentiation and integration of power series, since they’re just regular polynomial-style. No trigonometric identities, logarithms or other tricky integrals here. Just the plain rule for positive powers of x. For differentiation,…

Read More

Monads are part of C++, and are in your code

19 September, 202219 September, 2022

For several reasons, I didn’t get a CppCon ticket this year, but since I live in the Denver metro area I did go on the Friday, which is free to attend. Herb’s closing keynote (Cpp2) was thought provoking. At one point though, he said, Think about the words and the…

Read More
©2026 Why is a raven like a writing desk? | WordPress Theme by SuperbThemes