std::disjunction and Variadic Type Traits in C++
std::disjunction is a compile-time utility introduced in
C++17 that acts as a logical OR operator for variadic type traits. This
article explores how std::disjunction evaluates type traits
across parameter packs, detailing its mechanism for short-circuiting
template instantiations, its role in preventing compilation errors, and
its practical application in template metaprogramming, SFINAE, and
concept constraints.
The Purpose of
std::disjunction
In C++ template metaprogramming, developers frequently need to query whether at least one type in a variadic parameter pack satisfies a particular condition. Prior to C++17, implementing a logical OR across a variadic pack required writing recursive template specializations or using fold expressions with static boolean values.
std::disjunction, defined in the
<type_traits> header, standardizes this pattern. It
accepts a variable number of trait classes as template arguments and
inherits directly from either:
- The first type trait in the pack whose
::valueistrue. - The last type trait in the pack if all evaluate to
false. std::false_typeif the parameter pack is empty.
Short-Circuit Evaluation at Compile Time
The defining feature of std::disjunction is compile-time
short-circuit evaluation.
When using fold expressions such as
(Traits<Ts>::value || ...), the compiler must
instantiate Traits<Ts> for every type Ts
in the pack before performing the logical OR fold. In contrast,
std::disjunction instantiates traits lazily from left to
right. Once it encounters a trait where value evaluates to
true, instantiation stops.
This short-circuit behavior provides two major advantages:
- Compilation Performance: By avoiding unnecessary
template instantiations,
std::disjunctionsignificantly reduces compiler memory usage and improves build times on large codebases. - Preventing SFINAE and Substitution Failures: If a trait is invalid or produces a hard compiler error for certain types, placing a guarding trait before it prevents the invalid trait from ever being instantiated.
Code Example: Validating Types in a Parameter Pack
A common use case is checking whether a given type T
matches any type within an allowed list of types:
#include <type_traits>
template <typename T, typename... AllowedTypes>
struct is_any_of : std::disjunction<std::is_same<T, AllowedTypes>...> {};
template <typename T, typename... AllowedTypes>
inline constexpr bool is_any_of_v = is_any_of<T, AllowedTypes...>::value;
// Usage:
static_assert(is_any_of_v<int, double, float, int>); // Passes: int matches
static_assert(!is_any_of_v<char, double, float, int>); // Passes: char is not allowedIn this example, if std::is_same<int, double> is
false, the compiler proceeds to
std::is_same<int, float>, and so forth. If the match
is found early, subsequent is_same instantiations are
skipped.
Safe Metaprogramming with Invalid Types
The short-circuiting behavior allows safe probing of type properties
that might otherwise cause a hard error. Consider checking if a type is
either a pointer or defines a nested ::value_type:
template <typename T>
struct has_value_type {
using type = typename T::value_type; // Ill-formed if T is not a class or lacks value_type
static constexpr bool value = true;
};
template <typename T>
using is_pointer_or_has_value_type = std::disjunction<
std::is_pointer<T>,
has_value_type<T>
>;If T is int*,
std::is_pointer<int*>::value is true.
Because of short-circuiting, has_value_type<int*> is
never instantiated, safely avoiding a compile error on the invalid
int*::value_type lookup.
Role in Function Constraints (SFINAE and Concepts)
std::disjunction integrates seamlessly with
std::enable_if and C++20 requires clauses to
constrain template overloads:
template <typename... Args>
requires std::disjunction_v<std::is_integral<Args>...>
void process_if_any_integral(Args... args) {
// Function is only enabled if at least one argument is an integral type
}Using std::disjunction_v provides an expressive,
readable mechanism for multi-type logic, ensuring robust interface
constraints while maintaining compiler efficiency.