C++ Type Traits & Template Metaprogramming
A type trait is typically a template that maps a type T to:
- A compile-time boolean (e.g., "does satisfy some property?")
- Another type (e.g., "strip references from ")
Standard Library Foundation
In the Standard Library (<type_traits>), boolean traits are implemented using std::true_type and std::false_type, which derive from std::bool_constant:
#include <type_traits>
using std::true_type;
using std::false_type;
// Conceptual underlying implementation:
template <bool B>
struct bool_constant {
static constexpr bool value = B;
constexpr operator bool() const noexcept { return value; }
};
using true_type = bool_constant<true>;
using false_type = bool_constant<false>;
[!INFO] Key Properties
true_type::valueevaluates totrue;false_type::valueevaluates tofalse.- Objects of these types are implicitly convertible to
bool, allowing syntax likeif constexpr (MyTrait()).
General Pattern for Custom Traits
- Provide a primary template that defaults to
std::false_type. - Provide specializations that inherit from
std::true_typefor matching target types.
1.1. Simple Concrete Example: IsInt
Goal: Create a trait IsInt that evaluates to true only for int, and false for everything else.
#include <iostream>
#include <type_traits>
// Primary template: default to false
template <typename T>
struct IsInt : std::false_type {};
// Specialization: int is true
template <>
struct IsInt<int> : std::true_type {};
template <typename T>
void describe_type() {
if constexpr (IsInt<T>()) {
std::cout << "This is int\n";
} else {
std::cout << "This is NOT int\n";
}
}
int main() {
describe_type<int>(); // Prints: This is int
describe_type<float>(); // Prints: This is NOT int
return 0;
}
[!NOTE] How
if constexprWorks Hereif constexprevaluatesIsInt<T>()at compile time. Only the matching branch is compiled into the binary; the unselected branch is completely discarded.
1.2. Advanced Scenario: Port Filtering
This pattern applies type traits to inspect and filter template parameter packs and tuples at compile time.
1. Defining the Trait
// Primary template: default to false
template <class Tested_T>
struct SatisfiesDiagnosticServicePortConcept : std::false_type {};
// Positive specialization: matches diagnostic service ports
template <class ServiceData_T, diagnostic::types::ComponentId component_id_v>
struct SatisfiesDiagnosticServicePortConcept<
diagnostic::to_aos::DiagnosticGatewayToSwco<ServiceData_T, component_id_v>
> : std::true_type {};
- For any type
Tnot matching the template pattern,valueisfalse. - For types matching
diagnostic::to_aos::DiagnosticGatewayToSwco<ServiceData_T, component_id_v>,valueistrue.
2. Filtering a Single Port
template <class Port_T>
auto getDiagnosticServicePortOrEmpty(Port_T& port) {
using InterfaceType = typename diagnostic::utils::SampleValueType<Port_T>::Type;
if constexpr (SatisfiesDiagnosticServicePortConcept<InterfaceType>()) {
// Keeps the port: returns std::tuple<Port_T&>
return std::tuple<Port_T&>(port);
} else {
// Drops the port: returns std::tuple<>
return std::tuple<>{};
}
}
[!SUCCESS] Compile-time Execution
SatisfiesDiagnosticServicePortConcept<InterfaceType>()is evaluated at compile time.- If true, the return type is
std::tuple<Port_T&>.- If false, the return type is
std::tuple<>.- Zero runtime overheadâonly one branch is instantiated per
Port_T.
3. Filtering an Entire Tuple
template <typename PortTuple_T>
auto onlyDiagnosticServicePorts(const PortTuple_T& ports) {
return std::apply(
[](auto&... port) {
// For each port, returns either:
// - std::tuple<Port_T&> (1 element)
// - std::tuple<> (empty)
return std::tuple_cat(getDiagnosticServicePortOrEmpty(port)...);
},
ports
);
}
Breakdown:
std::apply: Unpacksportsinto individual arguments passed to the lambda function parameter packport....- Pack Expansion (
...): EvaluatesgetDiagnosticServicePortOrEmpty(port)for each individual port in sequence. std::tuple_cat: Concatenates all generated sub-tuples.
- Valid ports contribute
std::tuple<Port&>. - Invalid ports contribute
std::tuple<>(ignored).
- Result: A single flattened tuple containing only valid diagnostic service ports, maintaining original order.
1.3. Code Expansion Walkthrough
Given three ports:
ServicePortType p1;
NonServicePortType p2;
ServicePortType p3;
auto ports = std::tie(p1, p2, p3);
When invoking auto filtered = onlyDiagnosticServicePorts(ports);, the compiler expands the function template as follows:
auto onlyDiagnosticServicePorts(
const std::tuple<ServicePortType&, NonServicePortType&, ServicePortType&>& ports
) {
return std::apply(
// 1. Lambda expands parameters:
[](ServicePortType& p1, NonServicePortType& p2, ServicePortType& p3) {
// 2. Pack expansion expands tuple_cat arguments:
return std::tuple_cat(
getDiagnosticServicePortOrEmpty(p1), // Returns std::tuple<ServicePortType&>
getDiagnosticServicePortOrEmpty(p2), // Returns std::tuple<>
getDiagnosticServicePortOrEmpty(p3) // Returns std::tuple<ServicePortType&>
);
},
ports
);
}
References & Useful Links
Published Apr 14, 2026
â Back to articles