How std::negation Inverts Type Traits in C++
Introduced in C++17, std::negation is a template
metaprogramming utility that performs a logical NOT operation on a type
trait at compile time. This article explains how
std::negation operates under the hood, how it extracts and
inverts the boolean value of another trait, and why it is
preferred over a standard logical NOT operator (!) inside
template metaprogramming contexts.
The Mechanism of std::negation
Defined in the <type_traits> header,
std::negation<B> is a class template designed to wrap
another type trait, B, and invert its result. To achieve
this, std::negation relies on the standard
std::bool_constant helper.
Its conceptual definition is straightforward:
template<class B>
struct negation : std::bool_constant<!bool(B::value)> {};For any type trait B passed to
std::negation, B must define a static data
member named value that can be contextually converted to
bool. At compile time, the compiler evaluates
B::value, converts it to a bool, applies the
logical NOT operator (!), and passes the resulting boolean
value as the template argument to std::bool_constant.
Because it inherits from std::bool_constant,
std::negation exposes the standard type trait
interface:
value: A staticconstexpr boolrepresenting the inverted outcome.value_type: The typebool.type: The underlyingstd::integral_constantspecialization (std::bool_constant<!bool(B::value)>).operator bool(): A conversion operator that returns the inverted boolean value.operator(): A function call operator that returns the inverted boolean value.
In addition, C++17 provides the helper variable template
std::negation_v<B>:
template<class B>
inline constexpr bool negation_v = negation<B>::value;Basic Usage Example
Consider a scenario where a template requires a type that is explicitly not a pointer:
#include <iostream>
#include <type_traits>
template <typename T>
void process_value(T val) {
static_assert(std::negation_v<std::is_pointer<T>>,
"Pointers are not allowed in process_value.");
// Process the non-pointer value
}
int main() {
int x = 42;
process_value(x); // Compiles successfully
int* ptr = &x;
// process_value(ptr); // Static assertion fails: is_pointer evaluates to true, negation evaluates to false
}Why Use std::negation
Over the ! Operator
While writing !B::value works in many
static_assert or if constexpr expressions,
std::negation provides key advantages in
metaprogramming:
- First-Class Trait Compatibility: Unlike the scalar
expression
!B::value,std::negation<B>is a full type trait that can be passed as a type argument to other higher-order type traits, such asstd::conjunctionandstd::disjunction. - Short-Circuit Logical Operations: When combined
with
std::conjunctionorstd::disjunction,std::negationpreserves lazy instantiation. The compiler will not instantiate downstream traits in a logical sequence if an earlier condition already determines the outcome. - SFINAE and Concepts: When writing SFINAE-based
overloads with
std::enable_if_t, passing a trait type likestd::negation<std::is_same<T, U>>fits the pattern of expecting a type that contains a nested::valuemember.