Exploiting simultaneous pack-expansion inside macros

A curious solution, for a rare problem.

Even if I try to avoid macros as much as possible when writing regular c++ code, sometimes they are still a valuable option for getting things done. In this case it’s all about a convenience macro, which simplifies the process of declaring mocks with my own c++-20 mocking-framework mimic++.

If you don’t know mimic++ yet, that’s ok. It merely provides the hook for the problem. Let me just provide a little bit of context:

In mimic++ mocks are just template-types with a call-operator, which will also be used when mocking virtual functions. The latter requires some sort of redirect, which unfortunately leads to some tedious boilerplate-code. This is where the macro mentioned above comes into play:

// function_name: The function to be mocked (e.g. "foo")
// return_type: The return type of that function (e.g. "void")
// param_list: The param-list (e.g. "()" or "(int, std::string&)")
#define MOCK_METHOD(function_name, return_type, param_list) ... // the actual definition is not relevant here

Consider the following interface:

struct Interface
{
    virtual void foo(int) = 0;	// function to be mocked
};

To create a mock type, we could come up with this:

struct MyMock : Interface
{
    MOCK_METHOD(foo, void, (int));
};

The macro internally calls several other macros, until it eventually expands to the following (simplified) code:

struct MyMock : Interface
{
    mimicpp::Mock<void()> foo_{};         // the actual mock object
    void foo(int arg_i) override
    {
        foo_(std::forward<int&&>(arg_i)); // forwards the call to the mock object "foo_"
    }
};

NOTE For the sake of readability, I’ll omit the required \ characters at the end of each line in the macro definitions provided. All contiguous lines following a #define statement are part of that definition.

As you can see, the macro internally has to generate multiple parts of which the std::forward<int&&>(arg_i) is the relevant part for this post. This is generated by the macro MIMICPP_DETAIL_FORWARD_ARG (simplified version):

#define MIMICPP_DETAIL_FORWARD_ARG(param_type, param_name) std::forward<param_type>(param_name)

For the majority of cases, this is sufficient and get’s the job done.

Mocking variadic-template interfaces

NOTE Quick refresher: Variadic-templates are templates, which accept an arbitrary amount of template-arguments (see: cppreference).

Consider another interface:

template <typename... Args>
struct VariadicInterface
{
    virtual void foo(Args...) = 0;
};

At a first glance, this doesn’t look like an issue for the macro MOCK_METHOD, but due to how the internal macro MIMICPP_DETAIL_FORWARD_ARG is defined, this won’t compile. Let’s see why.

The internal macro is invoked like MIMICPP_DETAIL_FORWARD_ARG(Args..., some_name), which expands to std::forward<Args...>(some_name). This has two issues.

  • std::forward is not a variadic-template and thus does not accept an arbitrary amount of template-arguments and
  • some_name refers to a pack, which must be expanded via ....

What we actually need is this: std::forward<Args>(some_name)...

As this is a feature that I really want to support, I’ve accepted the challenge.

NOTE Interestingly, neither trompeloeil nor gmock currently support this (see: trompeloeil-example and gmock-example).

Solving the problem

NOTE Admittedly, my preprocessor-skills are very limited, so I do not know whether a simple macro-solution exists for that kind of purpose. In the following I’ll focus on solutions within the actual c++-language.

To be able to actually solve that problem, I first had to understand it.

There are two possible forms, which param_type can have:

  • Either in form of T, which then denotes a (possibly cv-ref qualified) type, or
  • in form of T..., which then requires a pack-expansion.

param_name on the other hand is always just a plain identifier, which in the first case can be treated as regular param or — in the second case — must also be treated as a pack.

An attempt has been made…

The first (rather naïve) idea I came up with, was to detect whether the param_type argument denotes an actual type-identifier or a pack and — depending on the outcome — doing one thing or the other. But this was more challenging than anticipated, as there is no official type-trait or any other support from the stl or language. Another issue is, that when I do pass param_type to an actual template/concept I would lose the information whether it was a pack or not. So I experimented with some kind of in-place requires-statement:

#define FORWARDING_MACRO(param_type, param_name)
    if constexpr (requires{ sizeof...(param_type); }) { // intention: does the operator ``sizeof...`` form a valid expression?
        std::forward<param_type>(some_name)...;
    }
    else {
        std::forward<param_type>(some_name)
    }

Well, as this is just plain wrong syntax, and not some kind of substitution-failure, this didn’t work. But at least this lead me to another idea.

A solution

By definition, pack-expansion (via ...) can encompass multiple packs; the only requirement is, that they have the same length. So, instead of conditionally detecting whether an expansion is necessary… Can I simply do a pack-expansion in any case and let the compiler decide, whether it expands a single pack or multiple packs simultaneously? That would require some kind of promotion: From a type to a pack.

The trick is to somehow pass the param_type into a variadic-template. A function for example could then take over the forwarding job:

#define FORWARDING_MACRO(param_type, param_name) forwarding_function<param_type>(param_name)

But, wait. That won’t work either, as we still have to conditionally expand the param_name. Thankfully, c++ has a feature, which acts like a function, but can also provide access to the outer scope: Lambdas to the rescue!

Let’s see how this can look like:

template <typename... Args>
struct type_list {};

#define FORWARDING_MACRO(param_type, param_name)
    [&]<template... types>(type_list<types...>) { // note the & capture
        std::forward<types>(param_name)...;       // unfortunately that's not very useful...
    }(type_list<param_type>{})                    // invoke the lambda immediately

That looks quite promising, but we are not done yet, because the lambda actually does nothing useful. The question is, what shall I return from that lambda? In fact, I decided to always create a std::tuple with appropriate references as elements and simply return that from the lambda, as this resulted in just a few simple changes in the surrounding macros.

Eventually the solution looks like this:

#define FORWARDING_MACRO(param_type, param_name)
    [&]<template... types>(type_list<types...>) {
        return std::forward_as_tuple(std::forward<types>(some_name)...);
    }(type_list<param_type>{})

// FORWARDING_MACRO(int, someName) expands to this:
[&]<template... types>(type_list<types...>) {
    return std::forward_as_tuple(std::forward<types>(someName)...); // ``types`` is a pack, but ``someName`` is just a regular param, thus only ``types`` is expanded.
}(type_list<int>{})

// FORWARDING_MACRO(Args..., someName) expands to this:
[&]<template... types>(type_list<types...>) {
    return std::forward_as_tuple(std::forward<types>(someName)...); // Both, ``types`` and ``someName``, are packs (guaranteed to be of same length),
}(type_list<Args...>{})                                             // thus the compiler expands them both simultaniously.

Conclusion

The whole story is definitely an edge-case one does not normally have to solve very often, but I actually found it quite entertaining. It also makes me a little bit proud, that mimic++ supports a feature which other well-known alternatives have their struggle with.

Hopefully, that little story was also interesting for you, too. Feel free to leave a comment and share your thoughts! I also encourage you to check out mimic++ as your next mocking framework, and I look forward to hearing about your experiences!

See you the next time, Dominic

Share: X (Twitter) Facebook LinkedIn