DEV Community

Ram Nad for IOTA, IIITS

Posted on • Edited on

C++ Templates and SFINAE

Recently, I came across a problem while looking for good first issues on GitHub. I would like to share the problem and its solution with you in this post. Just for clarity, we would work with a simpler version of the problem.

Suppose there is a template class Container that takes a class as Backend conforming to a particular interface. This Container acts as a wrapper on the Backend providing it with extra functionalities.

template<typename Backend>
class Container {
    private:
    Backend b;
    int data;
    int data1, data2;

    public:
    Container(int data, int data1, int data2) : data(data), data1(data1), data2(data2) {}

    int get() {
        return b.getValue(data);
    }
};

Now all the Backends need to have the following particular function,

class Backend1 {
    public:
    int getValue(int data);
};

This function is called by a get() function inside our container. The problem is that we need to change this interface to add up new functionalities. So, we now need to call getValue() with 2 parameters, instead of 1. So, our interface should now be:

int getValue(int data1, int data2);

We cannot make this change suddenly, because many users and backends depend upon the old functionality. Therefore, for the backends that support new interface we will call getValue with two parameters else we would call it with one.

We need to somehow statically(that is at compile-time) detect if any backend supports the new interface we use it, else we should stick with older interface.

Now, this is where the magic of C++ templates come to picture.

class SupportedBackend {
    public:
    int getValue(int data1, int data2) {
        return data1 + data2;
    }
};

class UnsupportedBackend {
    public:
    int getValue(int data) {
        return data + 1;
    }
};

So, among above two backends, SupportedBackend supports new interface whereas UnsupportedBackend does not. (I could not come up with any other descriptive name)

Here, is the code that can do the work for us,

#include <type_traits>

template <class T>
class check_new_interface {
  template <class R>
  static auto check(int) -> decltype(std::declval<R>().getValue(0, 0));

  template <class>
  static double check(double);

 public:
  static constexpr bool value = sizeof(decltype(check<T>(0))) == sizeof(int);
};

check_new_interface<T>::value would be true if T supports new interface else it will be false.

Let us now understand the above code.

So, check_new_interface class has two template member functions with same name (but different signatures). Now the value of value depends upon the following expression: sizeof(decltype(check<T>(0))) == sizeof(int). This is where we need to understand a lot of things.

First, sizeof and decltype do not evaluate the expression they are called with. So, the following program would only print 4 (that is sizeof(int)) instead of hello4. (Did you notice that we did not define the check functions although we called it the above expression)

#include <cstdio>

int main() { printf("%lu", sizeof(std::printf("hello"))); }

Second, when C++ sees check<T>(0) expression, it trys to find a proper function declaration that matches this call. There are two options, so template function overload resolution comes into picture. As, 0 is an integer literal, so C++ would first try to associate this call with function that takes integer as its parameter. If it cannot do so it will then check other options. For example,

#include <iostream>

long echo_with_type(double a) {
  std::cout << "double: " << a << std::endl;
  return a;
}

void first_echo(int a) { echo_with_type(a); }

int echo_with_type(int a) {
  std::cout << "int: " << a << std::endl;
  return a;
}

void second_echo(int a) { echo_with_type(a); }

int main() {
  first_echo(2);
  second_echo(2);
  return 0;
}

If, you run the above code, you would find that first output would be double: 2 and second would be int: 2. This is because when first_echo function is called, it does not find any echo_with_type function with integer parameter so it calls the one which takes double as parameter, but second time it finds one with integer and therefore call its.

Similarly, in our situation for check<T>(0) compiler would try to substitute T into the first template before the second one (for similar reasons as above program). Now, this is where the real magic happens. When any type is substituted in place of R, then only if that type has a member function with signature getValue(int, int) the substitution would succeed. Else it will fail. So, in our case when we will substitute SupportedBackend then this substitution would be succeed but in case of UnsupportedBackend it will fail. (Sorry, I am skipping few details here, like what is declval and why we use it but we can get into that sometimes later.)

Now, if C++ did not support SFINAE, then the compiler would have thrown an error here. But luckily it does so we can continue.

SFINAE - Substitution Failure is not an Error

Basically SFINAE means that if a template substitution fails then compiler does not throw error but continues to find a valid template substitution until it succeeds. (If it does not find any then ofcourse it will throw error.)

So, in our case as well compiler continues to find a valid substitution for check<T>(0) [when T = UnsupportedBackend]. Because first template substitution failed, it will move to the second template and succeed. But when it succeeds for the second template then return type of check<T> would be double instead of int. (Where as in case of T = SupportedBackend it would be int)

So, it is clear that the check_new_interface::value would be true in case of SupportedBackend and false in case of UnsupportedBackend.

Now, we may be tempted to implement get() as follows:

int get() {
    if(check_new_interface<Backend>::value) {
        return b.getValue(data1, data2);
    } else {
        return b.getValue(data);
    }
}

But, compiler would give errors if we try to do this. The reason is although compiler knows for any given Backend whether if part will exceute or else part but it needs to generate code for both of them. So, obviously it will fail.

The solution again would be to use function overloading here. std::true_type and std::false_type are specialized types of class template std::integral_constant defined as following:

typedef std::integral_constant<bool, true> true_type;

typedef std::integral_constant<bool, false> false_type;
private:

inline int get(std::false_type) { return b.getValue(data); }

inline int get(std::true_type) { return b.getValue(data1, data2); }

public:

int get() {
return get(
    std::integral_constant<bool, check_new_interface<Backend>::value>());
}

Now, this would work flawlessly because, when compiler generates code for get() function then, it only needs to compile one of get(std::false_type) or get(std::true_type) depending upon the value of check_new_interface<Backend>::value.

So, finally we have our solution ready. You can find the complete code here.

I hope that you got to learn something new in this post. Thanks for reading.

Top comments (0)