Skip to content

C++17 Logical Type Trait Composition

Overview

std::conjunction, std::disjunction, and std::negation are templates introduced in C++17 within <type_traits> for logical composition of type traits. They are compile-time versions of logical AND (&&), OR (||), and NOT (!), supporting short-circuit evaluation — skipping subsequent trait instantiations when the result is already determined, avoiding compile errors and unnecessary template expansion.

Basic Definitions

cpp
#include <type_traits>

// conjunction: true when all traits are true (short-circuit AND)
template <class... Bs>
struct conjunction : std::true_type {};

template <class B>
struct conjunction<B> : B {};

template <class B, class... Bs>
struct conjunction<B, Bs...>
    : std::conditional_t<bool(B::value), conjunction<Bs...>, B> {};

// disjunction: true when any trait is true (short-circuit OR)
template <class... Bs>
struct disjunction : std::false_type {};

template <class B>
struct disjunction<B> : B {};

template <class B, class... Bs>
struct disjunction<B, Bs...>
    : std::conditional_t<bool(B::value), B, disjunction<Bs...>> {};

// negation: logical NOT
template <class B>
struct negation : std::bool_constant<!bool(B::value)> {};

Basic Usage

cpp
#include <type_traits>
#include <iostream>

int main() {
    // conjunction: all traits must be true
    static_assert(std::conjunction<
        std::is_integral<int>,
        std::is_signed<int>,
        std::is_convertible<int, long>
    >::value, "all must be true");

    // disjunction: any trait being true suffices
    static_assert(std::disjunction<
        std::is_integral<int>,
        std::is_floating_point<int>
    >::value, "at least one must be true");

    // negation: logical NOT
    static_assert(std::negation<
        std::is_pointer<int>
    >::value, "int must not be a pointer");

    std::cout << "all assertions passed\n";
}

Short-Circuit Evaluation

This is the key difference from using &&/|| directly:

cpp
#include <type_traits>

// Short-circuit: when is_integral<int> is true, is_same<int, int*> is not instantiated
// Even if is_same<int, int*> had issues with incomplete types, it's skipped
static_assert(std::conjunction<
    std::is_integral<int>,             // true → continue
    std::is_same<int, int>             // true → result is true
>::value);

// Comparison with &&: both operands are instantiated
// conjunction stops at the first false
static_assert(std::is_integral<int>::value &&
              std::is_same<int, int>::value);  // both instantiated

Practical benefit — avoiding errors in SFINAE:

cpp
#include <type_traits>
#include <iostream>

// Problem: if T is not a class type, &T::value causes a compile error
// conjunction short-circuits to avoid unnecessary instantiation

// Unsafe (no short-circuit guarantee)
// template <typename T>
// using safe_check = std::bool_constant<T::value && std::is_class<T>::value>;
// If T is not a class, T::value is instantiated first, causing an error

// Safe (conjunction short-circuits)
template <typename T>
using safe_check = std::conjunction<
    std::is_class<T>,           // check if class first
    std::bool_constant<T::value> // only check T::value if class
>;

struct Good { static constexpr bool value = true; };
struct Bad { };

int main() {
    static_assert(safe_check<Good>::value);   // OK
    static_assert(!safe_check<int>::value);   // OK, short-circuits past T::value
    static_assert(!safe_check<Bad>::value);   // OK

    std::cout << "all safe checks passed\n";
}

Convenience Alias Templates

C++17 provides handy alias templates:

cpp
#include <type_traits>
#include <iostream>

int main() {
    // conjunction_v: get bool value directly
    static_assert(std::conjunction_v<
        std::is_integral<int>,
        std::is_signed<int>
    >);

    // disjunction_v
    static_assert(std::disjunction_v<
        std::is_integral<double>,
        std::is_floating_point<double>
    >);

    // negation_v
    static_assert(std::negation_v<
        std::is_pointer<double>
    >);

    std::cout << "alias templates work\n";
}

Practical Use Cases

SFINAE Constraints

cpp
#include <type_traits>
#include <iostream>
#include <string>

// Accept only arithmetic types except bool
template <typename T>
std::enable_if_t<std::conjunction_v<
    std::is_arithmetic<T>,
    std::negation<std::is_same<T, bool>>
>, T>
safe_multiply(T a, T b) {
    return a * b;
}

// Accept only move-constructible but not copy-constructible types
template <typename T>
std::enable_if_t<std::conjunction_v<
    std::is_move_constructible<T>,
    std::negation<std::is_copy_constructible<T>>
>, void>
process(T&& val) {
    T moved = std::move(val);
}

int main() {
    std::cout << safe_multiply(3, 4) << "\n";      // 12
    std::cout << safe_multiply(2.5, 4.0) << "\n";  // 10
    // safe_multiply(true, false);  // Compile error: bool excluded
}

Template Specialization

cpp
#include <type_traits>
#include <iostream>
#include <vector>
#include <list>

// Optimized at() for random-access iterator containers
template <typename Container>
typename std::enable_if_t<
    std::conjunction_v<
        std::is_same<typename Container::iterator_category,
                     std::random_access_iterator_tag>,
        std::negation<std::is_const<Container>>
    >,
    typename Container::reference
> unsafe_at(Container& c, size_t i) {
    // Random access: O(1)
    return c[i];
}

template <typename Container>
typename std::enable_if_t<
    std::negation_v<
        std::is_same<typename Container::iterator_category,
                     std::random_access_iterator_tag>
    >,
    typename Container::reference
> unsafe_at(Container& c, size_t i) {
    // Non-random access: O(n)
    auto it = c.begin();
    std::advance(it, i);
    return *it;
}

int main() {
    std::vector<int> v = {10, 20, 30};
    std::cout << unsafe_at(v, 1) << "\n";  // 20

    std::list<int> l = {100, 200, 300};
    std::cout << unsafe_at(l, 2) << "\n";  // 300
}

Concept Simulation (Pre-C++20)

cpp
#include <type_traits>
#include <iostream>
#include <string>
#include <sstream>

// Simulate C++20 concept: Printable
template <typename T>
using is_printable = std::conjunction<
    std::is_object<T>,
    std::negation<std::is_pointer<T>>,
    std::negation<std::is_array<T>>
>;

template <typename T>
std::enable_if_t<is_printable<T>::value>
smart_print(const T& val) {
    std::cout << val << "\n";
}

// Specialization for pointers
template <typename T>
std::enable_if_t<std::is_pointer_v<T>>
smart_print(T ptr) {
    if (ptr) {
        std::cout << *ptr << "\n";
    } else {
        std::cout << "(null)\n";
    }
}

int main() {
    smart_print(42);           // 42
    smart_print("hello");      // hello
    int x = 100;
    smart_print(&x);           // 100
}

conjunction vs &&

Featurestd::conjunction&&
Evaluation timeCompile-timeCompile-time
Short-circuitSupported (skips subsequent instantiation)Not guaranteed (all may be instantiated)
Error handlingShort-circuit avoids SFINAE errorsMay trigger unexpected compile errors
ReadabilityClearer in template metaprogrammingMore intuitive in simple cases
Return typestd::bool_constantbool

Compiler Support

CompilerMinimum VersionNotes
GCC5.0Full support
Clang3.5Full support
MSVC19.0 (VS 2015)Full support

Note: conjunction/disjunction/negation were available as extensions in many compilers since C++11 and were standardized in C++17. All modern compilers fully support them.

Best Practices

  • Prefer in SFINAE constraints: Use instead of &&/||/! to avoid compile errors from inconsistent short-circuiting.
  • Use _v suffix: conjunction_v<Bs...> is cleaner than conjunction<Bs...>::value.
  • Compose multiple type traits: Create complex type constraints for precise template specialization control.
  • Pre-Concepts constraint tool: Before C++20, this was the primary mechanism for simulating concepts.

Common Pitfalls

cpp
// Pitfall 1: conjunction short-circuits but && may not
// conjunction stops at the first false
// && may instantiate all operands (depends on compiler optimization)

// Pitfall 2: type trait's value member
// conjunction template parameters must be types inheriting from true_type/false_type
// Cannot use bool directly
// std::conjunction<std::true_type, true>  // Compile error!

// Pitfall 3: empty parameter list
static_assert(std::conjunction<>::value);   // true (default inherits true_type)
static_assert(!std::disjunction<>::value);  // false (default inherits false_type)

// Pitfall 4: comparison with fold expressions
// C++17 fold expressions can also do logical composition but don't guarantee short-circuit
// template <typename... Bs>
// using conjunction_fold = std::bool_constant<(Bs::value && ...)>;
// This does NOT guarantee short-circuit!

Released under the MIT License