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
94: if constexpr for Compile-Time Branching
If you've been spending any time with templates, you've probably hit a wall where you want the code to do one thing for an int and something completely different for a std::string or a custom class. In the old days, we had to use some truly arcane techniques like SFINAE (Substitution Failure Is Not An Error) or std::enable_if to make this happen. It was, frankly, a nightmare to read.
Wait, why can't I just use a regular if statement?
This is the first thing everyone asks. You might think, "If I'm inside a template, and I check the type of T, why can't a normal if just handle it?"
The problem is that a regular if is a runtime construct. Even if the condition is false, the compiler still tries to compile the code inside both the if and the else blocks. If that code doesn't make sense for the type you're using, the compiler will throw an error before the program even runs.
template <typename T>
void print_it(T value) {
if (std::is_pointer_v<T>) {
// If T is an int, this line will fail to compile
// because you can't dereference an int!
std::cout << *value << "\n";
} else {
std::cout << value << "\n";
}
}
Even if you call print_it(10), the compiler sees *value and panics because T is an int. That's where if constexpr comes in. It tells the compiler: "Evaluate this condition right now, and completely discard the branch that isn't taken."
How does this actually look in practice?
When you use if constexpr, the discarded branch is not instantiated. It's as if that code doesn't even exist for that specific version of the template. It makes your template logic look like normal C++ instead of a puzzle.
Look at how we fix the pointer example from above:
template <typename T>
void print_it(T value) {
if constexpr (std::is_pointer_v<T>) {
// This block is totally ignored if T is not a pointer
std::cout << "Pointer value: " << *value << "\n";
} else {
// This block is ignored if T IS a pointer
std::cout << "Direct value: " << value << "\n";
}
}
I love this because it keeps the logic contained in one function. I don't have to write three different overloads of the same function just to handle a few type differences.
Does the discarded code still have to be valid C++?
Yes, and this is a nuance that trips people up. The code in the discarded branch must be syntactically correct. You can't just put random gibberish in there.
However, it doesn't have to be semantically valid for the type. In our example, *value is valid C++ syntax (it's a dereference operation). The compiler is fine with that. It only fails when it tries to actually apply that operation to an int. Since if constexpr prevents that instantiation, we're in the clear.
Just remember: the condition inside the if constexpr must be a constant expression. You can't use a variable that changes while the program is running; it has to be something the compiler can figure out during the build process, like std::is_integral_v<T> or a constexpr boolean.
📋 Practical Task
Exercise: Building a Type-Aware Logger for Pointers and Values
You are building a logging utility that needs to handle both raw values and pointers to values. If the user passes a pointer, the logger should automatically dereference it to log the actual value. If they pass a value, it should log it directly.
Your Task: Implement a template function called log_data that takes a single argument of type T. Use if constexpr and the <type_traits> header to implement the following logic:
- If
Tis a pointer (usestd::is_pointer_v<T>), print:"Logging pointer value: [dereferenced value]" - Otherwise, print:
"Logging direct value: [value]"
Test your function with both an int and an int* to ensure the compiler is correctly discarding the inappropriate branches.
There are no comments for now.