Loading...

SFINAE Hell: detecting template methods

10 years ago

One of the cool thing about C++ is all these checks you can do at compile time. With compile time checks you can not only write safer code, but also faster code for the simple reason that alls the checks you do at compile time will not have to be done at runtime.

A classic SFINAE trick is to detect if a class has a method.

Let’s take a simple class:

struct boom { void bam(void) { /* something */ } };

What about a function that would accept any kind of object, but call bam when and only when the function exists, and this, at no-runtime costs and with absolutely no risk of running into a pure virtual function call error?

We will see how to write a class that will be true when the method is present and false otherwise.

Simple case

This classic case is very easy to write, especially in C++ 11:

struct not_boom {};

template <typename T>
struct has_bam_method
{
template <class, class> class checker;

template <typename C>
static std::true_type test(checker<C, decltype(&C::bam)> *);

template <typename C>
static std::false_type test(...);

typedef decltype(test<T>(nullptr)) type;
static const bool value = std::is_same<std::true_type, decltype(test<T>(nullptr))>::value;
};

static_assert(has_bam_method<boom>::value, "error");
static_assert(!has_bam_method<not_boom>::value, "error");

Note that this checker doesn’t really validate the exact profile of the function. That part is trivial and is left as an exercise to the reader (Writing that kind of things immediately makes your the post feel more scientific, doesn’t it?).

The type definition is useful if you want to use this class with Boost.MPL or another TMP toolkit.

Now, this post is not about this. This blog post is about how a simple modification to the boom class sent me to SFINAE Hell and beyond.

If you have a little piece of wood, now would be the right time to bite it: it will help with the pain.

Really, it should be easy

If your boom class becomes:

struct boom
{
template <typename T>
void bam(T)
{
/* something */
}
};

Then the above code no longer works because it doesn’t know which template parameter to give and therefore fails.

Fine! will you say, I will force the template parameter:

template <typename C>
static std::true_type test(checker<C, decltype(&C::adjust<boom>)> *);

That will not compile friend, because you need to inform the compiler that it is actually a template method, instead write:

template <typename C>
static std::true_type test(checker<C, decltype(&C:: template bam<boom>)> *);

That is however quite limited, because your function will only work now for type boom. The trick to get around this is to add a dummy type that will force template instance.

The code becomes

template <typename T>
struct has_bam_method
{
template <class, class> class checker;

struct dummy {};

template <typename C>
static std::true_type test(checker<C, decltype(&C:: template bam<dummy>)> *);

template <typename C>
static std::false_type test(...);

typedef decltype(test<T>(nullptr)) type;
static const bool value = std::is_same<std::true_type, decltype(test<T>(nullptr))>::value;
};

This will work in clang, but neither in gcc or MSVC.

Making it work in gcc

The problem you will have in gcc is that your dummy class must support the operations your template class has. In our example, our code being empty, it will always work, but if boom becomes:

struct boom
{
template <typename T>
void bam(T t)
{
t.something();
}
};

Then dummy has to support something. There are at least two workarounds:

  1. Mock all needed methods in dummy, this is not very flexible
  2. Take the template as a parameter, which might break some of your code calling has_bam_method (and you might not which type to specify anyway).

In our example:

struct dummy { void something() {} };

For the first workaround and:

template <typename T, typename MethodParameter>
struct has_bam_method
{
/// ...

template <typename C>
static std::true_type test(checker<C, decltype(&C:: template bam<MethodParameter>)> *);

/// ...
};

For the second work around.

You can also mix the two by making the detector take a template parameter and supply a dummy.

Making it work in MSVC

Unless you like internal compiler errors you will probably want to try a different kind of code in MSVC. Let’s cut to the chase, this is the function you will have to write in MSVC to make it work:

template <typename C>
static std::true_type test(checker<C, decltype(std::declval<C>().bam(dummy()))> *);

To be honest, I prefer it more as I find it easier to read. So why not use it in clang and gcc? Because for some reason both compilers reject it (tried in clang 3.4 and gcc 4.8 which are our production compilers, if you have any explaination why, feel free to share!).

Let me be clear, you will have to write a #ifdef #else #endif macro to make a version that works in MSVC, clang and gcc as none of the compilers can agree on a single version.

You will note it required dummy to be default constructable, which is not a problem when we use a dummy type but can be if you want to opt for the “let’s provide the type as a template parameter” approach.

If you don’t provide dummy as a parameter the substitution will fail and your method detector will not work. The nice effect is that it also work even if bam takes a reference as a parameter.

If it is a pointer, of course, you can just pass nullptr and get rid of the dummy trick.

Fortunately we can do better

After finishing the above code I wrote the infamous Professeur Falcou (yes, I know people) about this and he offered me that solution instead:

template <typename C, typename P>
static auto test(P * p) -> decltype(std::declval<C>().bam(*p), std::true_type());

It has the double advantage of getting rid of the checker structure and working in MSVC (at least 12), clang and gcc (with a couple of spurious internal compiler errors)!

Streamlined, the has_bam_method class becomes:

template <typename T>
struct has_bam_method
{
struct dummy { /* something */ };

template <typename C, typename P>
static auto test(P * p) -> decltype(std::declval<C>().bam(*p), std::true_type());

template <typename, typename>
static std::false_type test(...);

typedef decltype(test<T, dummy>(nullptr)) type;
static const bool value = std::is_same<std::true_type, decltype(test<T, dummy>(nullptr))>::value;
};

If your compiler crashes on the Professeur Falcou version (especially if this is a gcc crash) you are most likely running out of memory.

Why not use Boost.TTI?

Boost.TTI doesn’t work with template methods. However, for functions with template parameters you should use Boost.TTI (that is what we do in quasardb).

On of the great features of Boost.TTI is that it gladly support exact profile match and it actually has a lot of introspection features.

Keep in mind it will not be able to detect the existence of the method in a inherited class (neither does our code, and yes, this modification is left as an exercise to the reader…).

What a lovely week-end

What I love with SFINAE is that it is always very pleasant, like eating rusted nails for breakfast.

However it is often worth the cost as once the code is done, using it is very easy and doesn’t require understanding all the internals.

How we use it

There are many places in quasardb where we use method and members detection, namely:

  • In serialization code it helps us detect if a class supports certain features without writing specific support code. This makes the code faster and avoids metastasizing serialization code.
  • We can iterate on all the members of a class (with Boost.Fusion‘s help) and call a method whether this method exists or not. This helps making sure you didn’t forget to call a method after adding a member.
  • For the protocol we use it to trigger different actions depending on the presence of certain data members in messages.

Do you see other usages for such tricks? Do not hesitate to share!

Top