Using SFINAE to Control Overload Resolution

Learn how SFINAE allows templates to remove themselves from overload resolution based on their arguments, and apply it in practical examples.
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Illustration representing computer hardware
Ryan McCombe
Ryan McCombe
Posted

In this lesson, we will learn about the SFINAE paradigm, which is an abbreviation of Substitution Failure Is Not An Error. SFINAE allows templates to remove themselves from consideration during overload resolution if their arguments do not meet certain requirements.

The lesson covers causing substitution failures, using std::enable_if, and provides a practical example of applying SFINAE with type traits.

Why do we need SFINAE?

In our previous lesson on overload resolution, we saw that the compiler can select a function that will cause a compilation error, even if a more appropriate function was available.

In the following example, we are calling Render with a double. There are two candidates - a template function that accepts any type, and a Render function that accepts a float.

Given that the template can generate a function that will be an exact match for the double argument, the compiler elects to use the template, resulting in a compilation error:

#include <iostream>

template <typename T>
void Render(T Object) {
  std::cout << "Template Function Called\n";
  Object.Render();
}

void Render(float x) {
  std::cout << "Non-Template Function Called\n";
}

int main() {
  Render(3.14);
}
error: left of '.Render' must have class/struct/union

Naturally, we could address that at our invocation of Render() by explicitly casting our float to a double but we’d rather fix this in our template.

Rather than forcing other developers to perform unnecessary explicit casts, a better design would be to prevent our template from inserting itself into situations where it can’t possibly work.

There are two main ways a template can remove itself from consideration if the arguments do not meet it's requirements:

  • Using the SFINAE paradigm, which we cover in this lesson
  • Using C++20 concepts, which we cover in the next lesson

What is SFINAE?

In our previous lesson, we said that if a template causes a compilation error within its function body, that does not prevent it from being selected within the overload resolution process.

Indeed, that is what is happening in the previous example. The compiler is selecting our Render template, even though it will cause a compilation error, and even though another candidate is available that would have worked.

There is an exception to this - when we instantiate a template, invalid code that appears within the template parameter list, function return type, or function parameter list, are handled differently.

If an issue appears in the locations we’ve marked with *** in the following example, that is referred to as a substitution failure:

template <***>
*** Render(***) {
  // Function Body
}

The SFINAE paradigm is an abbreviation for substitution failure is not an error. It means that if a substitution failure occurs when instantiating a template with an expression like Render(SomeArgument) the compiler will not throw an error.

Instead, it will simply remove the template from the list of viable candidates to serve that expression. From there, overload resolution proceeds as normal, attempting to find the next best candidate.

Causing Substitution Failures

SFINAE gives us a fairly inelegant way to solve the problem we introduced at the beginning of the lesson. We can set up our template such that, if an attempt is made to instantiate it with an unsupported type, we intentionally cause a substitution failure.

Variations of this idea often involve introducing an additional template or function parameter that will be invalid if a template argument doesn’t meet our requirements.

In the following example, we add a function parameter called y , whose type will be a pointer to the type returned by the first parameter’s Render method.

This type is unused within our body, and we don’t expect consumers to provide an argument for it, so we give it a default value of nullptr:

template <typename T>
void Render(
  T Object,
  decltype(Object.Render())* y = nullptr
) {
  std::cout << "Template Function Called\n";
  Object.Render();
}

Now, when Object does not have a Render() method, we’ll create a substitution failure, removing our template from consideration.

Below, our double argument does not have a Render method so, after the substitution failure, we fall back to using the Render(float) overload.

Our Rock{} argument does have a Render method, so our template is instantiated with y having a type of void*. A void pointer is a valid type in C++ (we’ll cover what it’s used for later in the course) so our template is selected as the preferred candidate:

#include <iostream>

struct Rock {
  void Render() {
    std::cout << "Rendering a Rock";
  }
};

template <typename T>
void Render(
  T Object,
  decltype(Object.Render())* y = nullptr
) {
  std::cout << "Template Function Called\n";
  Object.Render();
}

void Render(float x) {
  std::cout << "Non-Template Function Called\n";
}

int main() {
  Render(3.14);
  Render(Rock{});
}
Non-Template Function Called
Template Function Called
Rendering a Rock

Preview: C++20 Concepts

If the above code feels overly complicated, don’t worry - it is. It’s widely acknowledged that SFINAE is a complex and inelegant solution to this problem.

So much so that C++20 introduced a new language feature called concepts that removes the need for SFINAE-based techniques.

In the next lesson, we’ll demonstrate how we could create a concept called Renderable, which expresses a set of requirements of a type. Then, we can specify a template type has those requirements simply by replacing typename in our argument list with our concept’s name:

template <Renderable T>
void Render(T Object) {
  Object.Render();
}

However, concepts are not yet widely implemented in the projects we’ll be working on, so knowledge of SFINAE is likely to be useful for some time to come.

Using std::enable_if

The std::enable_if template within the <type_traits> library is designed to make the conditional creation of substitution failures slightly more elegant. It accepts two template arguments - a boolean expression that can be evaluated at compile time, and a type. It then returns a struct.

If our boolean was true, the struct would have a type static member matching the type we provided as the first argument. If the boolean was false, there will be no such member, resulting in invalid code if we try to use it.

#include <type_traits>

int main() {
  // this will be an int
  std::enable_if<true, int>::type IntA{1};
  
  // this will be a compilation error
  std::enable_if<false, int>::type IntB{2};
}
error: 'type': is not a member of 'std::enable_if<false,int>'

As with most standard library traits, we can alternatively use the std::enable_if_t variation orm to access the type directly:

#include <type_traits>

int main() {
  // this will be an int
  std::enable_if<true, int>::type IntA{1};

  // this will be an int too
  std::enable_if_t<true, int> IntB{2};
}

A Practical Example

Let’s use SFINAE in a more realistic project structure. Below, we’ve reintroduced the is_renderable type trait we created in the previous lesson. Our Rock type satisfies this trait, whilst no other type does.

Without SFINAE, our attempt to call Render() with a double fails as before:

// Rendering.h
#pragma once
#include <type_traits>
#include <iostream>

template <typename>
struct is_renderable : std::false_type {};

template <typename T>
constexpr bool is_renderable_v{
  is_renderable<T>::value};
  
template <typename T>
void Render(T Object) {
  std::cout << "Template Function Called\n";
  Object.Render();
}
// Rock.h
#include <iostream>
#include "Rendering.h"
#pragma once

struct Rock {
  void Render() {
    std::cout << "Rendering a Rock";
  }
};

template <>
struct is_renderable<Rock>
  : std::true_type {};
// main.cpp
#include <iostream>
#include "Rendering.h"
#include "Rock.h"

void Render(float x) {
  std::cout << "Non-Template Function Called\n";
}

int main() {
  Render(3.14);
  Render(Rock{});
}
error: left of '.Render' must have class/struct/union

Within Rendering.h, we can now apply our knowledge of SFINAE with the help of std::enable_if and our is_renderable trait. We’ll introduce an additional, unused template parameter to the Render() template.

If our first template parameter (the T typename) satisfies the is_renderable concept, our second parameter will have a type of int. If T does not satisfy is_renderable, our second parameter will have no type at all.

In other words, if T does not satisfy the is_renderable trait, our template will generate a substitution failure, thereby removing it from the candidate list:

// Rendering.h
#pragma once
#include <type_traits>
#include <iostream>

template <typename>
struct is_renderable : std::false_type {};

template <typename T>
constexpr bool is_renderable_v{
  is_renderable<T>::value};
  
template <typename T, std::enable_if_t<
  is_renderable<T>::value, int> = 0>
void Render(T Object) {
  std::cout << "Template Function Called\n";
  Object.Render();
}

Now, our template is no longer interfering with the code in main.cpp, and our program works as intended:

// main.cpp (Unchanged)
#include <iostream>
#include "Rendering.h"
#include "Rock.h"

void Render(float x) {
  std::cout << "Non-Template Function Called\n";
}

int main() {
  Render(3.14);
  Render(Rock{});
}
Non-Template Function Called
Template Function Called
Rendering a Rock

Summary

In this lesson, we learned about the SFINAE paradigm, and how we can use it. The key points to remember are:

  • SFINAE stands for "Substitution Failure Is Not An Error" and allows invalid code in specific parts of a template to remove it from overload resolution instead of causing a compilation error
  • What SFINAE is and how it allows templates to remove themselves from overload resolution if their arguments don't meet the requirements
  • How to cause substitution failures by introducing additional template or function parameters that become invalid if a template argument doesn't meet requirements
  • Using std::enable_if to make the conditional creation of substitution failures easier
  • Applying SFINAE in a practical example with custom type traits and std::enable_if
  • A preview of C++20 concepts as a cleaner alternative to SFINAE

Was this lesson useful?

Next Lesson

Concepts in C++20

Learn how to use C++20 concepts to constrain template parameters, improve error messages, and enhance code readability.
Illustration representing computer hardware
Ryan McCombe
Ryan McCombe
Posted
Lesson Contents

Using SFINAE to Control Overload Resolution

Learn how SFINAE allows templates to remove themselves from overload resolution based on their arguments, and apply it in practical examples.

A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Type Traits and Concepts
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, unlimited access

This course includes:

  • 124 Lessons
  • 550+ Code Samples
  • 96% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Concepts in C++20

Learn how to use C++20 concepts to constrain template parameters, improve error messages, and enhance code readability.
Illustration representing computer hardware
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved