Skip to Content
Course content

241: Concepts vs SFINAE for Constraint Checking

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I want to build a small utility library that can "dump" the state of an object to the console. Ideally, I only want this function to be available for types that actually know how to print themselves—meaning they have a member function called print(). If I try to call it on an int or a std::vector, I don't want a cryptic 100-line compiler error; I want the compiler to just tell me, "Hey, this type isn't printable."

The "It Just Works" Phase

My first instinct is to keep it simple. Let's just write a template function and see where it breaks.

template <typename T>
void dump(const T& obj) {
    obj.print();
}

struct User {
    void print() const { std::cout << "User active\n"; }
};

int main() {
    User u;
    dump(u);      // Works great.
    dump(42);     // Error!
}

When I compile this, the dump(42) call fails. But here's the problem: the compiler doesn't say "int doesn't satisfy the requirements of dump." Instead, it dives deep into the function body and tells me that int has no member named print. In a tiny example, that's fine. In a project with 50 layers of template nesting, that's a nightmare. I want the constraint to be part of the function's signature, not its implementation.

Wrestling with enable_if

Before C++20, we had SFINAE (Substitution Failure Is Not An Error). I remember the first time I encountered std::enable_if; I thought I had accidentally started writing a different language. Let's try to implement the constraint the "old way."

First, I need a trait to detect if print() exists. I'll use the void_t trick, which is the standard way to do this in C++17.

template <typename T, typename = void>
struct has_print : std::false_type {};

template <typename T>
struct has_print<T, std::void_t<decltype(std::declval<T>().print())>> : std::true_type {};

template <typename T>
typename std::enable_if<has_print<T>::value>::type
dump(const T& obj) {
    obj.print();
}

Look at that return type. typename std::enable_if<has_print<T>::value>::type. It's a mouthful. I'm essentially telling the compiler: "If has_print is false, this function signature is invalid. Please just ignore it and look for another overload instead of crashing."

It works! Now, if I call dump(42), the compiler says the function is not found. But there's a catch: if I have *only* this version of dump, the error message is just "no matching function for call to dump." It doesn't tell me why it failed—it just says it couldn't find any function that fit. It's better than the first version, but it's still opaque.

Cleaning up the mess with Concepts

This is where C++20 Concepts come in. I can replace that entire has_print struct and the enable_if monstrosity with a single concept definition. I want to define what it "means" to be printable.

template <typename T>
concept Printable = requires(T t) {
    { t.print() } -> std::same_as<void>;
};

void dump(Printable auto const& obj) {
    obj.print();
}

This is leagues better. I've moved the logic from "magic template substitution" to a named requirement. I'm using a requires expression to explicitly say: "T must have a member function print() that returns void."

Now, when I call dump(42), the compiler is actually helpful. It tells me: note: constraints not satisfied and specifically points out that int does not satisfy the Printable concept because it lacks a print() method. I've gone from "This code is broken" to "This type doesn't meet the requirement."

Why we actually care about the difference

You might be wondering if SFINAE is completely dead. Not quite, but for constraint checking, it's effectively obsolete. Here is why I prefer Concepts every time:

  • Readability: Printable auto is a sentence. std::enable_if_t<...> is a puzzle.
  • Compilation Speed: The compiler can evaluate concepts much faster than it can attempt to instantiate multiple complex SFINAE overloads.
  • Developer Experience: The error messages are actionable. You know exactly which constraint failed and why.

SFINAE was a hack—a clever use of a compiler quirk. Concepts are a first-class language feature designed for this exact purpose. If you're on C++20 or later, stop using enable_if for constraints. Your future self (and your teammates) will thank you.




📋 Practical Task

Implement a "Mathematical" Constraint for a Power Function

You are building a math library. You want to create a function calculate_power(base, exponent), but it should only be available for types that support both multiplication (*) and the assignment operator (=). You want to ensure the base and exponent are of the same type.

Your Task:

  1. Define a concept named Multiplicable that ensures a type T can be multiplied by itself and assigned back to a variable of type T.
  2. Implement the calculate_power function using this concept to constrain the template parameters.
  3. The function should take (T base, int exp) and return T.
  4. Test your code with a custom ComplexNumber struct (which supports *) and a std::string (which does not). Verify that the std::string version fails to compile with a clear constraint error.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.