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 ::value is true.
  • The last type trait in the pack if all evaluate to false.
  • std::false_type if 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:

  1. Compilation Performance: By avoiding unnecessary template instantiations, std::disjunction significantly reduces compiler memory usage and improves build times on large codebases.
  2. 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 allowed

In 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.