Having fun with Detection idiom and macros in C++

Introduction

The detection idiom is now well known in C++. However, some implementations necessitate a lot of boilerplate, and I do believe that macros may help here. So, let’s having fun with the detection idiom and macros in C++!

The goal with detection idiom in C++

The detection idiom is used to know at compile time if an expression is valid, or not. For example, it can be used to know if a class has a function, an attribute, an operator…

Our objective will be to write something like

    constexpr auto additionnable = is_valid((x, y), x + y);
    
    static_assert(additionnable(a, b));
    static_assert(!additionnable(a, c));
    static_assert(additionnable(c, c));
    static_assert(!additionnable(c, a));

It is easy to read. The is_valid function (actually, it will be a macro) just returns a helper function with 2 arguments that return true_type if the expression is possible or returns false_type otherwise.

Basic detection idiom in C++

To implement the idiom detection in C++, you may use polymorphic lambdas. If you chose this solution, you will need 3 things.

  1. The case where the expression is not possible
  2. The case where the expression is possible
  3. A wrapper to make things easier.

The first case is really easy

template<typename F>
std::false_type is_valid_impl(...);

It is a function template that takes a function as a first parameter and returns false.

The second case is a bit more complicated.

template<typename F, typename ...Xs>
constexpr auto is_valid_impl(Xs&& ...xs) -> decltype(std::declval<F>()(FWD(xs)...), void(), std::true_type());

The idea is simple, we test to call F with the given arguments, and we return true. The void() is used because the result of F(xs...) may returns something that implements operator,.

The third case is easy, it is just a simple wrapper.

template<typename F>
constexpr auto is_valid_f(F) {
    return [](auto &&...xs) -> decltype(is_valid_impl<F>(FWD(xs)...)){ return {}; };
};

The usage is easy, but requires a lot of boilerplate …

    int a = 0;
    int b = 1;
    std::string c;
    
    constexpr auto additionnable = is_valid_f([](auto a, auto b) -> decltype(a + b){return a + b;});
        
    static_assert(additionnable(a, b));
    static_assert(!additionnable(a, c));
    static_assert(additionnable(c, c));
    static_assert(!additionnable(c, a));

Making thing easier with simple macros

A simple macro could simplify things.

#define is_valid2(a, b, expr) is_valid_f([](auto a, auto b) -> decltype(expr) {return expr;})
constexpr auto additionnable = is_valid2(x, y, x + y);

It is now much easier to read, however, it requires you to make a is_valid macro for each number of arguments, and it is not a good thing at all…

Going further with macros

Basics

The first thing you must understand is that recursive macros are theoretically impossible. Macros work by expansion. Each time you will expand a macro, the underlying called macros will be _painted blue_ and will not be called again. For example, if you use #define X X, when you will write X, it will not recurse until your death, it will just replace X by … X.

However, you may use indirect recursion ! Let’s make a first try

#define X() X_I()
#define X_I() X()
X()

Do you think it will work? The answer is no. You expand X(), so you call X_I(). The call to X_I expands to X which is painted blue inside this expansion…

What we must do is to defer the call to X and the call to X_I and force expansion several times :

#define EMPTY()
#define DEFER(...) __VA_ARGS__ EMPTY()
#define EXPAND(...) __VA_ARGS__
#define EVAL1(...) EXPAND(EXPAND(EXPAND(__VA_ARGS__)))
#define EVAL2(...) EVAL1(EVAL1(EVAL1(__VA_ARGS__)))
#define EVAL3(...) EVAL2(EVAL2(EVAL2(__VA_ARGS__)))
#define EVAL(...) EVAL3(EVAL3(EVAL3(__VA_ARGS__)))

#define X() DEFER(X_I)()
#define X_I() DEFER(X)()

Now, we are ready. We must define an interface for our macro. I propose the following :

#define is_valid(args, expr)

With such an interface, you will not be able to pass a list of arguments directly, so you must wrap them. A usual way to wrap arguments with macro is a parenthesis, so you will write is_valid((a, b, c, d), expr).

To deal with such list, the easiest way is to call another macro

#define MACRO(a, b, c, d)
#define is_valid(args, expr) MACRO args
is_valid((a, b, c, d), expr);

This expression will lead to MACRO(a, b, c, d)

The complicate task

What will be difficult here is to create a macro that takes some arguments in a list and transform each value into auto &&x,

The goal here is to transform MACRO(a, b, c, d) into [](auto &&a, auto &&b, auto &&c, auto &&d);

An interesting idea is to create a MAP macro that takes 2 arguments. The MACRO to apply and the arguments. The macro to apply here could be a MAKE_AUTO(x).

#define MAKE_AUTO(x) auto &&x

Going that way, it is easy to understand that MAKE_AUTO(a) expands to auto &&a.

So, let’s design MAP. As I explained before, it will take 2 arguments, so we can lay:

 #define MAP(MACRO, args)

This macro will work as follows. Take the first argument and apply the macro to it. When the tail is not empty, perform recursion on the tail. Here is my definition of MAP macro.

#define MAP(MACRO, args)                                                       \
 #define MAP(MACRO, args)                                                       \
  MACRO(HEAD args)                                                             \
  WHEN(NOT(IS_EMPTY(TAIL args)))(DEFER_TWICE(MAP_I)(MACRO, TAIL args))

#define MAP_I(MACRO, args) , DEFER_TWICE(MAP)(MACRO, args)

So, here are the explanations. The beginning is easy, you just call the MACRO with the first argument. Here is the definition of HEAD and TAILS

#define HEAD(x, ...) x
#define TAIL(x, ...) (__VA_ARGS__)

HEAD takes a list and returns the first element, TAIL returns the list of all elements but the first.

The second line is where the black magic occurs. Here are some high-level explanations before showing the code.

  • WHEN(c)(expr) will expand to expr only if c == 1
  • NOT(x) will expands to 1 if x == 0 and to 0 if x == 1
  • IS_EMPTY(args) will expand to 1 if the list of args is empty or 0 otherwise
  • DEFER_TWICE is needed because WHEN already make one expansion.
  • MAP_I adds a , and call MAP again

When you develop using macros, I advice you to use these 2 helpers:

#define CAT_IMPL(x, y) x##y
#define CAT(x, y) CAT_IMPL(x, y)

#define STRINGIFY_IMPL(...) #__VA_ARGS__
#define STRINGIFY(...) STRINGIFY_IMPL(__VA_ARGS__)

The CAT macro will concat x and y, and STRINGIFY will transform arguments into a string. That may help you for debugging for example. The IMPL versions does not make expansion of arguments, but the not _IMPL versions do.

#define X Y
STRINGIFY(X) // "Y"
STRINGIFY_IMPL(X) // "X"

The first Macro we are going to define is NOT. It is easy to define

#define NOT1 0
#define NOT0 1
#define NOT(x) CAT_IMPL(NOT, x)

We use the CAT_IMPL and not the CAT version because x will be expanded here, so we do not need another level of expansion. For people coming from a functional world, it may remain pattern matching.

The second macro we are going to define is DEFER_TWICE. The idea follows the DEFER macro:

#define DEFER_TWICE(...) __VA_ARGS__ DEFER(EMPTY)()

Let’s define the WHEN macro.

#define WHEN0(...)
#define WHEN1(...) __VA_ARGS__
#define WHEN(c) CAT_IMPL(WHEN, c)

It is easy, we do pattern matching to a function that does nothing when c is 0, and we expand the other arguments when c is 1.

The most difficult macro to define is IS_EMPTY. I told you about an end sentinel. A simple end sentinel to use with macro is a parenthesizes. So, we just add () at the end of arguments. For example : (a, b, c, ()) represents a list that contain [a, b, c].

IS_EMPTY(x) must return 1 only when x looks like (()). Here is my solution

#define CHECK_IMPL(x, n, ...) n
#define CHECK(...) CHECK_IMPL(__VA_ARGS__, 0)
#define PROBE(...) ~, 1

#define IS_EMPTY_IMPL(args) PROBE args
#define IS_EMPTY(args) CHECK(IS_EMPTY_IMPL(HEAD args))

Now that we have an almost working solution (I said almost because MAP will not work with an empty list, but it is not difficult to fix that), we can make the definition for our is_valid macro.

#define is_valid(args, expr)                                                   \
  is_valid_f(                                                                  \
      [](EVAL(MAP(MAKE_AUTO, ADD_END_SENTINEL args))) -> decltype(expr) {      \
        return expr;                                                           \
      })

The ADD_END_SENTINEL macro is easy. It just appends () to the list.

#define ADD_END_SENTINEL(...) (__VA_ARGS__, ())

And now, we can enjoy our new function !

struct Cat {
  void meow();
};

struct Dog {
  void bark();
};

int main() {
  int a = 0;
  int b = 1;
  std::string c;
  Cat cat;
  Dog dog;

  constexpr auto additionnable1 = is_valid((... xs), ((... + xs)));
  constexpr auto additionnable2 = is_valid((x, y), x + y);
  constexpr auto has_meow = is_valid((x), x.meow());

  static_assert(additionnable1(a, b));
  static_assert(!additionnable1(a, c));
  static_assert(additionnable1(c, c));
  static_assert(!additionnable2(c, a));
  static_assert(has_meow(cat));
  static_assert(!has_meow(dog));
}

Conclusion

I hope you liked this article and you learned something. If you have any questions, feel free to ask, I would be glad to answer.

If you want to try it, here is the full implementation.

calendar June 27, 2021 category C++


Leave a Reply


Scroll Top