Article
Compile-Time Short-Circuiting with std::conjunction
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, andstd::negationbring lazy boolean logic to the type system.- In C++20 and later, prefer concepts for clearer syntax and inherent laziness.