Templates

SFINAE

A template that looks correct may fail if the compiler tries to substitute a particular type into it. Under normal rules, the failure would be a hard error and the program would not compile. SFINAE changes this by having the compiler simply discard the failing candidate and try another viable function. The name is an abbreviation for the rule that allows this: Substitution Failure Is Not An Error.

A concrete example follows with a template that assumes its argument supports a particular operation. The primary template tries to call a member called size.

#include <iostream>
#include <string>
#include <type_traits>

template <typename T>
// Define or declare this function with the shown parameters
auto getSize(T value) -> decltype(value.size())
{
    return value.size();
}

int main()
{
    std::cout << getSize(std::string{"hello"}) << '\n';
    return 0;
}

The trailing return type is decltype(value.size()). When the argument is a std::string, the expression is well formed, the return type is std::size_t, and the function body executes and prints 5. The template works for any type that provides a size member.

The same template but with an int:

#include <iostream>
#include <type_traits>

template <typename T>
// Derive the type from this expression without evaluating it
auto getSize(T value) -> decltype(value.size())
{
    return value.size();
}

int main()
{
    std::cout << getSize(42) << '\n';
    return 0;
}

Substitution of an int will produce the expression 42.size(). An int does not support this member, and substitution fails. Under normal rules, it would result in a compile time error.

However, because SFINAE is present, the candidate is silently dropped out of the overload set.

If there is no other available function, the call is still ill formed, but the error is no longer a hard error on the template, but the absence of a valid overload.

Another overload could have been provided for exactly the case in which the first candidate is rejected. Using the trait std::is_integral along with std::enable_if allows us to create such an overload:

#include <iostream>
#include <string>
#include <type_traits>

template <typename T>
// Remove this overload when its type requirement fails
auto getSize(T value) -> typename std::enable_if<!std::is_integral<T>::value, decltype(value.size())>::type
{
    return value.size();
}

template <typename T>
auto getSize(T value) -> typename std::enable_if<std::is_integral<T>::value, int>::type
{
    return -1;
}

int main()
{
    std::cout << getSize(std::string{"hello"}) << '\n';
    std::cout << getSize(42) << '\n';
    return 0;
}

The first template is constrained by std::enable_if<!std::is_integral<T>::value, decltype(value.size())>. When the type T is not an integral type, the enable_if yields a valid type and the function is added to the overload set.

When T is integral, the enable_if substitution fails, and the function is rejected.

The second template has the opposite constraint and returns -1. A string call then prints 5, and an int call returns -1.

This choice mechanism could also be implemented with C++17's std::enable_if_t which provides a more concise syntax:

#include <iostream>
#include <string>
#include <type_traits>

template <typename T>
// Query this type property at compile time
auto getSize(T value) -> std::enable_if_t<!std::is_integral_v<T>, decltype(value.size())>
{
    return value.size();
}

template <typename T>
auto getSize(T value) -> std::enable_if_t<std::is_integral_v<T>, int>
{
    return -1;
}

int main()
{
    std::cout << getSize(std::string{"hello"}) << '\n';
    std::cout << getSize(42) << '\n';
    return 0;
}

std::enable_if_t is an alias template for std::enable_if, and it yields its second parameter's type when the condition is true and results in a substitution failure otherwise. It is identical to the prior solution in that only the remaining function will be selected.

SFINAE only takes place during substitution of template arguments into the signature of function templates. An overload, once accepted into the overload set, is not subject to SFINAE. Any errors occurring in the function body are normal compilation errors. In essence, SFINAE protection is only for the resolution phase of an overloaded function call.

Exercise: SFINAEStarter code, hints and solutionOpenClose
Guided practiceBeginner · 10–15 min

Practice: SFINAE

Work through the mission, reveal hints only when needed, and compare your result with the model solution.

Your challenge

Add getSize(true). Identify the surviving overload and predict the value without trying to invoke a nonexistent size member on bool.

Mission checklist

  1. Run the starter as a baseline, then predict the change requested in Your challenge.
  2. Make that change in your editor; use the lesson-specific hints if you need help.
  3. Compile and run your answer, then compare with the worked solution and explain the result.

Starter program

This is the working baseline, not the completed answer. Compile as C++20, then make the change described above.

#include <iostream>
#include <string>
#include <type_traits>

template <typename T>
// Remove this overload when its type requirement fails
auto getSize(T value) -> typename std::enable_if<!std::is_integral<T>::value, decltype(value.size())>::type
{
    return value.size();
}

template <typename T>
auto getSize(T value) -> typename std::enable_if<std::is_integral<T>::value, int>::type
{
    return -1;
}

int main()
{
    std::cout << getSize(std::string{"hello"}) << '\n';
    std::cout << getSize(42) << '\n';
    return 0;
}

Hints

Hint 1

For std::string, the size() expression is valid and the non-integral overload can participate. For int, that overload is removed and the integral overload remains.

Hint 2

Use true as the new argument and inspect std::is_integral<bool>. The selected integral overload returns its constant result instead of accessing a size member.

Worked solution

The additional getSize(true) call deduces bool, an integral type. Substitution removes the size()-based overload and leaves the integral overload, which returns -1. No nonexistent bool.size() is executed.

#include <iostream>
#include <string>
#include <type_traits>

template <typename T>
// Remove this overload when its type requirement fails
auto getSize(T value) -> typename std::enable_if<!std::is_integral<T>::value, decltype(value.size())>::type
{
    return value.size();
}

template <typename T>
auto getSize(T value) -> typename std::enable_if<std::is_integral<T>::value, int>::type
{
    return -1;
}

int main()
{
    std::cout << getSize(std::string{"hello"}) << '\n';
    std::cout << getSize(42) << '\n';
    std::cout << getSize(true) << '\n';
    return 0;
}

Example output

5
-1
-1

0 comments

No comments yet. Be the first.

Add a comment

About CppLearn

CppLearn is an interactive, free reference for modern C++. In each lesson, you'll find out how the language works, rather than just lines of code that you can copy and paste. We cover concepts starting from beginner lessons on fundamentals and proceed to templates, move semantics, smart pointers, and areas that usually trap or scare C++ learners.