Thursday, 9 July 2026

Using some modern C++ features to avoid macros


Recently I had an opportunity to use some non-obvious C++ language features, which in those days doesn't happen that often to me. ๐Ÿ˜ž To be frank, lately I'm doing more QML and JavaScript programming than pure C++, so I was more than happy to be able to  jump into the challenge.

The (arguably small) problem I had, was that I had to define a seemingly endless amount of callback functions all doing basically just the same thing:
  auto callb = [this](std::expected<uint32_t, ErrCode> result) -> void
  {
    if (result.has_value()) {
      emit okNotification(result.value());
      logCallback("BlaBlaCallback " + std::to_string(result.value()));
    } else  {
      emit errorNotification(std::to_underlying(result.error()));
      logCallback("BlaBlaCallback ERROR: " + std::to_string(std::to_underlying(result.error())));
    }
  };
As you can see, I was working with Qt and I used their signal/slot mechanism. In this context the emit clause sends a signal to all parties that subscribed for it. 

So basically we need a callback that looks into a std::expected value and decides what kind of a Qt signal has to be sent - an error notification or a value change one. Additionally, std::expected result can contain values of different types: integers of different ranges and signs, as well as floating point numbers and enum values.
 

1. The definition 


The first idea to come (because I'm oldskool?) was a macro. I even wrote one, but then I said to myself - come on, that's ugly, Bjarne working all the time to give us mechanism to make macros obsolete and now I write that abomination? So, ashamed, I deleted it immediately and started with a template-based solution. 

As templates are kinda oldskool themselves, I wanted to try polymorphic lambdas first, hoping that maybe compiler can figure out all that parametrization stuff for me and I will just write down the auto keyword and everything will be good in the world. So I wrote this piece of code:
  auto makeUint32Callback = [this](auto notifOk, auto notifErr, const std::string& label)
  {
    return
      [this, notifOk, notifErr, label](std::expected<uint32_t, ErrCode> result) -> void
      {
        if (result.has_value()) {
          emit (this->*notifOk)(result.value());
          logCallbacks(label + " " + std::to_string(result.value()));
        } else {
          emit (this->*notifErr)(std::to_underlying(result.error()));
          logCallbacks(label + " ERROR: " + std::to_string(std::to_underlying(result.error())));
        }
      };
  };
As you can see, I defined a helper function (a lambda in the local scope), which constructs the final lambda from provided parameters, which final lambda can then serve all callbacks reporting value changes for values of type uint32

Nice one, but what about other value types? Do we have to define makeInt32Callback(), makeUint8Callback(), makeFloatCallback() etc? We certainly don't want that! 

To the rescue comes a C++20 feature called templated lambdas. Don't fear, it's very simple, we just give a type parameter to the lambda and it's all we need here:
  auto makeTypedCallback = [this]<typename T>(auto notifOk, auto notifErr, const std::string& label) 
  {
    return
      [this, notifOk, notifErr, label](std::expected<T, HalEvent>result) -> void
      { 
        if (result.has_value()) {
          emit (this->*notifOk)(result.value());
          logCallbacks(label + " " + std::to_string(result.value()));
        } else {
          emit (this->*notifErr)(std::to_underlying(result.error()));
          logCallbacks(label + " ERROR: " + std::to_string(std::to_underlying(result.error())));
        }
      };
  };
As easy as that! We then just forward the type parameter T to the std::expected parameter definition and that's it. Now we even can go further and add support for enum types using an if constexpr construct inside of the lambda:
  auto makeMyCallback = [this]<typename T>(auto notifOk, auto notifErr, const std::string& label) 
  {
    return
      [this, notifOk, notifErr, label](std::expected<T, HalEventId> result) -> void 
        if (result.has_value()) {
          if constexpr (std::is_enum_v<T>) {
            emit (this->*notifOk)(std::to_underlying(result.value()));
            logCallbacks(label + " " + std::to_string(std::to_underlying(result.value())));
          } else {
            emit (this->*notifOk)(result.value());
            logCallbacks(label + " " + std::to_string(result.value()));
          }
        } else {
          emit (this->*notifErr)(std::to_underlying(result.error()));
          logCallbacks(label + " ERROR: " + std::to_string(std::to_underlying(result.error())));
        }
      };
  };
When seeing if constexpr compiler will decide which branch of the code has to be taken and which has to be ignored. And all that at compile time!
 

2. The invocation 


Now as we have our templated, polymorphic lambda ready, we just invoke it for each pair of notifications and we are done! But how exactly should we invoke a templated lambda? Somehow it's not that obvious. We could try to do it the standard way, just as we always have done with data structurers, i.e. like that:
  auto callb = makeMyCallback<OperationMode>(
                       &NotifThreadWrapper::operationModeChanged,
                       &NotifThreadWrapper::operationModeChangeError,
                       "OperationModeCallback");
But the compiler won't let us have it! ๐Ÿ˜ž The right way, however, is:
  auto callb = makeMyCallback.operator()<OperationMode>(
                       &NotifThreadWrapper::operationModeChanged,
                       &NotifThreadWrapper::operationModeChangeError,
                       "OperationModeCallback");
Why's that? Well, as it turns out, you cannot templatize a lambda as a whole, but you can only templatize its function call operator! An that is what the funny looking syntax does: lambda.operator()<T>(args), the type T comes after the call operator. 

If you think about it, it is quite logical, a lambda is just a generalized function, so you parametrize the function call, right? Because, come on, it's even declared like that: []<typename T>(T x) { /* ... */ } in the first place! 

On the other hand however it is just syntax, and the Standard Committee could add support for the first invocation as well. The chose not to. 
 

3. Extending it further 


Some time later I added some new callbacks taking a std::pair of something as parameter. How can we support this new callback type in the makeMyCallback() helper? Simple, another if constexpr will do it:
  ...
  else if constexpr (is_pair_v<T>)  // why not std::is_pair_v ???
  {
    emit (this->*notifOk)(result.value().first, result.value().second);
    logCallbacks(label + " " + std::to_string(result.value().first) + "/" +
                 std::to_string(result.value().second));
  }
However, because apparently there isn't a std::is_pair_v predicate in the standard library ๐Ÿ˜ฎ, I also had to provide following definitions:
  template <typename T>
  struct is_pair : std::false_type {};

  template <typename T1, typename T2>
  struct is_pair<std::pair<T1, T2>> : std::true_type {};

  template <typename T>
  inline constexpr bool is_pair_v = is_pair<T>::value;
Easy, but std::is_pair would be so much nicer! 

4. Wrap-up 


And there it is: a
  •   polymorphic,
  •   templated
lambda
  • returning a lambda,
thus clearly being a metafunction* (yay!). Or can we call it meta-lambda/metalambda? 

Now you know how to write yourself one and, what's maybe even more important, also how to invoke it and put it to good use instead of polluting your code with ugly macros! 

__
 * metafunctions - function returning other functions, it should be pretty clear? But it isn't. 

For example Boost MPL libraray ("...high-level C++ template metaprogramming framework of compile-time algorithms, sequences and metafunctions.") defines them quite differently:

"... A metafunction is a class or a class template that represents a function invocable at compile-time",

thus clearly meaning just a *constexpr* function. Hmmm... ๐Ÿค”

Other definition I've seen are "...functions that operate on types, rather than traditional runtime values" or "...Metafunctions are program elements (classes in C++) that can return computed types". This definition makes more sense than the MPL one, but how can we then name a lambda returning a lambda? 

Here I chose to stick to functional programming nomenclature where I first learned that concept:

"...A metafunction is simply a function that operates on other functions or on representations of functions, often at the meta‑level (i.e., reasoning about code, types, or syntax rather than just data)".

Of course, we could also use the (more correct) term "higher order functions", but "metafunction" sounds so much crispier 
๐Ÿ™‚, so I hope you will forgive me this little stretch of nomenclature. 

No comments: