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
61: SFINAE Explained
SFINAE—Substitution Failure Is Not An Error—is one of those terms that sounds like a magic spell until you actually see it in a compiler error log. At its heart, SFINAE is just a rule: if the compiler tries to plug a type into a template and it doesn't fit, it doesn't crash the whole build. It just says, "Okay, this specific overload isn't a match," and moves on to the next one.
I've spent far too many hours in my career fighting template errors because I didn't realize I was hitting a "hard" error instead of a "substitution" failure. The trick is knowing where to put the "failure" so the compiler ignores the function rather than complaining that your code is broken.
The goal: A smart print utility
Let's build something practical. I want a print_value() function. If a class has its own print() member function, I want to use that. If it doesn't (like an int or a std::string), I want to just pipe it straight to std::cout. This sounds simple, but in C++, you can't just put an if statement inside a template to check if a method exists; that's a compile-time check, not a runtime one.
My first mistake: The naive approach
When I first tackled this years ago, I tried something like this:
template <typename T>
void print_value(const T& value) {
// I thought I could just "try" this...
value.print();
}
This is a disaster. The moment I pass an int into this function, the compiler sees value.print(), realizes int has no such method, and throws a hard error. The build stops. This isn't SFINAE; this is just a broken program. To make SFINAE work, the failure has to happen in the function signature, not the function body.
Filtering overloads with std::enable_if
To fix this, we need two overloads of print_value(). One for types that can print themselves, and one for everything else. We can use std::enable_if to "turn off" an overload if a condition isn't met.
Here is how I'd set up the "fallback" version for types that don't have a print method:
#include <iostream>
#include <type_traits>
// This version is only enabled if the type is NOT a custom "printable" type
template <typename T>
typename std::enable_if<!std::is_floating_point<T>::value, void>
print_value(const T& value) {
std::cout < "Default print: " < value < std::endl;
}
Wait, that's not quite right. is_floating_point doesn't tell us if a class has a .print() method. To actually detect a member function, we need a "trait." This is where SFINAE really shines. We can create a helper that tries to call .print() in a way that fails silently.
Refining the detection with void_t
Since C++17, the cleanest way to do this is using std::void_t. It's a tiny utility that essentially says: "If all the types inside these parentheses are valid, this whole thing is just void."
Let's build the trait first, then the functions:
#include <iostream>
#include <type_traits>
// 1. The primary template: assume it doesn't have .print()
template <typename T, typename = void>
struct has_print_method : std::false_type {};
// 2. The specialization: if T::print exists, this one is preferred
template <typename T>
struct has_print_method<T, std::void_t<decltype(std::declval<T>().print())>> : std::true_type {};
// Now we use our trait to pick the overload
template <typename T>
typename std::enable_if<has_print_method<T>::value, void>
print_value(const T& value) {
std::cout < "Custom method: ";
value.print();
}
template <typename T>
typename std::enable_if<!has_print_method<T>::value, void>
print_value(const T& value) {
std::cout < "Default print: " < value < std::endl;
}
Notice what's happening in the specialization of has_print_method. The decltype(std::declval<T>().print()) part is the "Substitution" part of SFINAE. If T is an int, int().print() is illegal. Instead of crashing, the compiler simply decides that this specialization doesn't apply and falls back to the primary template (which is false_type).
I prefer this approach because it separates the detection logic (the trait) from the implementation logic (the functions). It keeps the code readable and prevents your function signatures from becoming giant, unmanageable blocks of template soup.
📋 Practical Task
Exercise: Building a "Clearable" Container Detector
In many libraries, some containers have a .clear() method (like std::vector) and some do not (like std::array or a raw pointer). Your task is to implement a SFINAE-based system to handle this.
Requirements:
- Create a trait named
has_clear_methodusingstd::void_tthat detects if a type has a.clear()member function. - Implement a function
reset_container(T& container). - If the container has a
.clear()method, the function should call it and print "Container cleared using .clear()". - If the container does NOT have a
.clear()method, the function should print "Container does not support .clear(), skipping". - Test your solution with a
std::vector<int>and astd::array<int, 5>to ensure both overloads are triggered correctly.
There are no comments for now.