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
60: Concepts and Constraints (C++20)
If you've spent any time writing templates in C++, you know the pain of the "template error wall of death." You pass the wrong type into a function, and the compiler gives you 200 lines of internal header traces before finally telling you that a type doesn't support the + operator. C++20 Concepts are essentially our way of telling the compiler: "Don't even try to instantiate this unless the type meets these specific requirements."
Why bother with Concepts when we have SFINAE?
Look, SFINAE (Substitution Failure Is Not An Error) worked for years, but let's be honest: std::enable_if_t is an eyesore. It's a hack. You end up burying your logic in return types or dummy template parameters, and it makes the function signature almost impossible to read at a glance. I've spent hours debugging enable_if logic just to get a simple overload to work.
Concepts turn those "hacks" into first-class language features. Instead of tricking the compiler into ignoring a function, you're explicitly stating the requirements. The biggest win isn't even the syntax—it's the error messages. When a Concept isn't met, the compiler tells you exactly which requirement failed, rather than dumping the entire template instantiation stack on your head.
How do I actually write and apply a custom Concept?
You use the concept keyword. Think of a concept as a named boolean predicate that the compiler evaluates at compile-time. You usually define it using a requires expression.
Let's say we're building a physics engine and we need a type that isn't just any number, but specifically something that can be added and multiplied. We can define a Numeric concept like this:
#include <concepts>
#include <iostream>
template<typename T>
concept Numeric = std::is_arithmetic_v<T> && requires(T a, T b) {
{ a + b } -> std::convertible_to<T>;
{ a * b } -> std::convertible_to<T>;
};
// Now we apply it. No more 'typename T'. Just 'Numeric T'.
template<Numeric T>
T calculate_force(T mass, T acceleration) {
return mass * acceleration;
}
int main() {
std::cout << calculate_force(10.5, 9.8) << "\n"; // Works great
// calculate_force("heavy", 9.8); // This would fail immediately with a clear error
}
Notice the { a + b } -> std::convertible_to<T> part. That's a compound requirement. It doesn't just check if the + operator exists; it checks if the result of that operation can be converted back into the type T. It's a very powerful way to enforce interface contracts.
Can I use Concepts to overload functions based on type properties?
Yes, and this is where it gets really useful. Before C++20, if you wanted a different implementation of a function for integers versus floating-point numbers, you had to use messy template specializations. Now, you can just overload the function using different concepts.
I often use this when I want a "fast path" for simple types and a "robust path" for complex ones. Check this out:
#include <concepts>
#include <iostream>
#include <vector>
template<typename T>
concept Iterable = requires(T t) {
{ t.begin() } -> std::input_iterator;
{ t.end() } -> std::input_iterator;
};
// Overload 1: For single numeric values
void process_data(std::integral auto value) {
std::cout << "Processing an integer: " <lt; value <lt; "\n";
}
// Overload 2: For floating point values
void process_data(std::floating_point auto value) {
std::cout << "Processing a float: " <lt; value <lt; "\n";
}
// Overload 3: For things we can loop over
void process_data(Iterable auto container) {
std::cout << "Processing a collection of size: " <lt; container.size() <lt; "\n";
}
int main() {
process_data(42); // Calls integral version
process_data(3.14); // Calls floating_point version
process_data(std::vector{1, 2, 3}); // Calls Iterable version
}
Using auto in the function parameter list combined with a concept is called an "Abbreviated Function Template." It's significantly cleaner than the old template<typename T> void func(T val) syntax and makes the code feel much more like a modern language.
📋 Practical Task
Exercise: Implementing a Constrained `find_max` for Comparable Types
Your goal is to create a generic find_max function that finds the largest element in a collection. However, to prevent this function from being used with types that can't actually be compared (like a custom User struct without an operator>), you must implement a custom Concept.
Requirements:
- Define a concept named
Comparable. It should ensure that the typeTsupports the>operator and that the result of that operation is convertible tobool. - Implement a function
find_maxthat takes astd::vector<T>. Use theComparableconcept to constrain the template parameterT. - Ensure the function handles empty vectors gracefully (e.g., by throwing an exception or returning a default).
- Test the function with a
std::vector<int>and astd::vector<std::string>to verify it works for different comparable types. - Try calling it with a vector of a custom
struct Point { int x, y; };(which has no comparison operator) to verify that the compiler now gives you a clear "concept not satisfied" error instead of a deep template trace.
There are no comments for now.