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
240: Writing Custom Concepts
I've been working on a logging utility for a project lately, and I ran into a classic C++ frustration. I wanted a generic function that could take any "Entity" object—whether it's a User, a Transaction, or a SystemLog—and print it to the console. Naturally, I started with a template.
template <typename T>
void log_entity(const T& entity) {
std::cout << "Log Entry: " << entity << "\n";
}
Dealing with the Template Wall
This looks fine on paper. But the second I tried to pass a std::vector<int> into log_entity, the compiler absolutely lost its mind. I got a 40-line error message essentially telling me that std::ostream doesn't know how to handle a vector.
The problem is that T is too generic. I don't want any type; I want a type that is actually printable. Now, I could use std::is_convertible or some SFINAE magic, but that's the old way. Since we're in C++20, I want the compiler to tell me "Hey, this type doesn't meet my requirements" before it even tries to instantiate the function body.
Drafting a Basic Requirement
I'll start by defining a concept. I want to ensure that the expression std::cout << value is valid. I'll call this Printable. Here is my first attempt:
template <typename T>
concept Printable = requires(T a) {
{ std::cout << a };
};
Wait, that didn't compile. The compiler is complaining that the expression { std::cout << a } doesn't return a type that's being checked. In a requires expression, if I wrap something in curly braces, I'm telling C++: "This expression must be valid, and I might care about its return type." Since I just care that it works, I can actually just list the expression without the braces, or I can specify the return type. Let's go with the simpler version:
template <typename T>
concept Printable = requires(T a) {
std::cout << a;
};
Now I can apply this to my function. Instead of typename T, I'll use Printable auto. It's much cleaner.
void log_entity(Printable auto const& entity) {
std::cout << "Log Entry: " << entity << "\n";
}
Now, when I pass that std::vector in again, the error is a single line: constraints not satisfied. Much better. But I realized I need more. A "Loggable Entity" in my system shouldn't just be printable; it should also have a get_id() method that returns something convertable to a string.
Layering Constraints
This is where custom concepts get powerful. I can combine requirements. I want my Loggable concept to require the Printable concept and a specific member function.
template <typename T>
concept Loggable = Printable<T> && requires(T a) {
{ a.get_id() } -> std::convertible_to<std::string>;
};
Look at that last line. I'm using a compound requirement. I'm not just saying a.get_id() must exist; I'm saying the result of that call must be convertible to a std::string. If I have a class that returns an int for the ID, this concept will fail unless I provide a way to make that int a string.
Putting it to the Test
Let's see if this actually holds up. I'll create a User class and a BrokenEntity class to see the difference.
struct User {
std::string get_id() const { return "user_123"; }
friend std::ostream& operator<<(std::ostream& os, const User& u) {
return os << "User(123)";
}
};
struct BrokenEntity {
// Missing get_id()
friend std::ostream& operator<<(std::ostream& os, const BrokenEntity& b) {
return os << "Broken";
}
};
void log_entity(Loggable auto const& entity) {
std::cout << "[" << entity.get_id() << "] " << entity << "\n";
}
If I call log_entity(User{}), it works perfectly. If I call log_entity(BrokenEntity{}), the compiler stops me immediately. It tells me BrokenEntity doesn't satisfy Loggable because it doesn't have a get_id() method. I've effectively moved the "debugging" phase from runtime (or 100-line template errors) to the very moment I wrote the function call.
📋 Practical Task
Implementing a "Numeric Range" Concept
You are building a statistics library. You need a function called calculate_variance that only works with types that behave like numbers. Specifically, your custom concept NumericRange must ensure that the type T satisfies the following:
- It must support the addition operator (
+). - It must support the multiplication operator (
*). - It must be
std::equality_comparable(you can use the standard concept for this). - It must have a member function
to_double()that returns adouble.
Your Task:
- Define the
NumericRangeconcept using arequiresexpression. - Create a
struct DataPointthat satisfies this concept. - Create a
struct InvalidPointthat fails the concept (e.g., missing theto_double()method). - Write a function
void print_variance(NumericRange auto val)that simply callsval.to_double()and prints it. - Ensure your code compiles when calling
print_variancewith aDataPoint, and identify why it would fail withInvalidPoint.
There are no comments for now.