Article

Compile-Time Short-Circuiting with std::conjunction

March 18, 2019

C++ short-circuits runtime expressions like if (a && b): if a is false, b is never evaluated. That avoids wasted work and prevents UB when the second operand depends on the first.

We often need the same behaviour with type traits. Assume a trait that detects a nested type T::alternate_type:

template<class, class = void>
struct has_alternate : std::false_type {};

template<class T>
struct has_alternate<T, std::void_t<typename T::alternate_type>>
    : std::true_type {};

A naïve if constexpr eagerly instantiates every trait:

if constexpr ( std::is_trivial<C>::value &&
                std::is_trivial<D>::value &&
                has_alternate<C>::value  &&
                has_alternate<D>::value ) {
}

Even when std::is_trivial<C> is false, the compiler still creates has_alternate<C>, slowing compilation and breaking SFINAE in some cases.

Lazy conjunction

std::conjunction evaluates its arguments one by one and stops at the first false:

template<class T>
using trivial_and_alt =
    std::conjunction<std::is_trivial<T>,
                     has_alternate<T>>;

Because the first trait guards the second, pointless instantiations disappear.

Using enable_if

template<class T,
         std::enable_if_t<
           std::conjunction<
             std::is_trivial<T>,
             has_alternate<wrapper<T>>
           >::value, int> = 0>
void fun(T const&, int);

template<class T>
void fun(T const&, void*);

For non-trivial T, the specialised overload is discarded before has_alternate is instantiated, so the fallback wins.

C++20 and later

Concepts make the syntax even cleaner. The requires clause is already lazy:

template<class T>
requires std::is_trivial_v<T> &&
         has_alternate<wrapper<T>>::value
void fun(T const&);

Overload ordering is automatic, eliminating the dummy parameter hack.

Takeaways

  • Runtime && short-circuits; compile-time && does not.
  • std::conjunction, std::disjunction, and std::negation bring lazy boolean logic to the type system.
  • In C++20 and later, prefer concepts for clearer syntax and inherent laziness.