C++
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
241: Concepts vs SFINAE for Constraint Checking
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 autois 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:
- Define a
conceptnamedMultiplicablethat ensures a typeTcan be multiplied by itself and assigned back to a variable of typeT. - Implement the
calculate_powerfunction using this concept to constrain the template parameters. - The function should take
(T base, int exp)and returnT. - Test your code with a custom
ComplexNumberstruct (which supports*) and astd::string(which does not). Verify that thestd::stringversion fails to compile with a clear constraint error.
There are no comments for now.