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
248: Compile-Time Constants with std::integral_constant
You've likely spent a lot of time with constexpr. It's great for making sure a value is computed at compile time. But there's a specific architectural need that constexpr doesn't quite hit: what if you need a value to behave like a type? That's where std::integral_constant comes in.
When we wrap a value in a type, we can use template specialization to make the compiler choose different code paths. This is the heart of "tag dispatching." I'm going to show you how to build a simple priority-based message processor that uses this technique to eliminate runtime branching.
Turning Values Into Types
Imagine we have three priority levels for a system: Low, Medium, and High. Instead of using an enum and a switch statement—which the compiler might optimize, but doesn't guarantee it will—we can define these priorities as types.
#include <iostream>
#include <type_traits>
// We define our priorities as types using std::integral_constant
using PriorityLow = std::integral_constant<int, 0>;
using PriorityMedium = std::integral_constant<int, 1>;
using PriorityHigh = std::integral_constant<int, 2>;
Now, PriorityHigh isn't just a variable equal to 2; it is a unique type. It has a static member value (which is 2) and a value_type (which is int). This is the "magic" that lets us use these in templates.
The Dispatching Logic
Now I want a Processor that handles these priorities differently. I'll use a template struct and specialize it for each priority type.
template <typename Priority>
struct MessageProcessor {
static void process() {
std::cout << "Processing with default priority logic\n";
}
};
// Specialization for High Priority
template <>
struct MessageProcessor<PriorityHigh> {
static void process() {
std::cout << "CRITICAL: Processing with high-priority immediate bypass!\n";
}
};
// Specialization for Low Priority
template <>
struct MessageProcessor<PriorityLow> {
static void process() {
std::cout << "Background: Processing with low-priority throttled logic\n";
}
};
Oops, I Forgot About Tag Dispatching
Here is where I usually trip up when I'm first implementing this. I'll try to write a wrapper function to make the API cleaner, but I'll accidentally pass the value instead of the type.
// My mistake: I'm trying to pass the value '2', not the type 'PriorityHigh'
template <int P>
void handle_message() {
MessageProcessor<P>::process(); // Error! P is an int, not a type.
}
I just realized I'm trying to use the int P as a template argument for MessageProcessor, but MessageProcessor expects a type. The compiler is going to scream at me here because an integer is not a type. To fix this, I need to pass the std::integral_constant itself as a parameter to the function. This is what we call a "tag."
The Final Compile-Time Switch
To fix the mistake, I'll change the function to take an instance of the priority type. Since the type carries the value, the compiler can resolve exactly which MessageProcessor specialization to use at the call site.
template <typename T>
void dispatch_message(T priority_tag) {
// The compiler looks at the type of priority_tag and
// picks the correct MessageProcessor specialization.
MessageProcessor<T>::process();
}
int main() {
// We pass an instance of the type.
// The actual object is empty, it's just a "tag".
dispatch_message(PriorityHigh{});
dispatch_message(PriorityLow{});
dispatch_message(PriorityMedium{}); // Uses the default template
return 0;
}
What's happening here is beautiful: there is no if or switch in the generated assembly for dispatch_message. The compiler sees PriorityHigh{}, knows that T is PriorityHigh, and jumps directly to the specialized process() method. It's effectively a zero-cost abstraction.
📋 Practical Task
Build a Storage Tier Selector
You are designing a data persistence layer that saves objects to different storage tiers based on how "hot" the data is: Hot (In-Memory), Warm (SSD), and Cold (Archive/HDD).
Your task is to implement this using std::integral_constant. Follow these requirements:
- Define three types using
std::integral_constant<int, ...>:TierHot,TierWarm, andTierCold. - Create a template struct
StorageHandler<T>with a static methodsave()that prints "Saving to generic storage". - Create template specializations for
TierHot(printing "Saving to RAM") andTierCold(printing "Saving to Tape Archive"). - Implement a function
persist_data(T tier_tag)that uses these tags to call the correctsave()method. - In your
main(), trigger thepersist_datafunction for all three tiers to verify that the specializations and the default case are working correctly.
There are no comments for now.