How std::not_fn Creates Negated Callables in C++

Introduced in C++17 and refined in C++20, std::not_fn is a standard library utility that wraps any callable entity to return a new function object representing its logical negation. This article explores how std::not_fn works under the hood, how it leverages modern C++ features like perfect forwarding and std::invoke to construct negated callables, and how updates in C++20 enhance its usability with compile-time execution and value category forwarding.

The Purpose of std::not_fn

Prior to C++17, negating predicates required clumsy utilities like std::not1 and std::not2. These utilities required the target callable to define specific nested types (such as argument_type or first_argument_type), making them incompatible with raw lambdas and arbitrary callables without extra boilerplate like std::ptr_fun.

std::not_fn, defined in the <functional> header, eliminates these constraints. It accepts any callable target—lambdas, free functions, member pointers, or custom functors—and constructs a wrapper object that inverts the boolean result of invoking the target.

Internal Mechanics and std::invoke

When calling std::not_fn(f), it constructs and returns an unspecified call wrapper object containing a decayed copy of the callable f. The wrapper's call operator (operator()) forwards incoming arguments directly to the stored callable using std::invoke.

The core operation inside the returned wrapper can be conceptually modeled as:

template <typename F>
class negated_callable {
    F f;
public:
    template <typename... Args>
    auto operator()(Args&&... args) & 
        -> decltype(!std::invoke(f, std::forward<Args>(args)...)) {
        return !std::invoke(f, std::forward<Args>(args)...);
    }

    template <typename... Args>
    auto operator()(Args&&... args) const & 
        -> decltype(!std::invoke(f, std::forward<Args>(args)...)) {
        return !std::invoke(f, std::forward<Args>(args)...);
    }

    template <typename... Args>
    auto operator()(Args&&... args) && 
        -> decltype(!std::invoke(std::move(f), std::forward<Args>(args)...)) {
        return !std::invoke(std::move(f), std::forward<Args>(args)...);
    }

    template <typename... Args>
    auto operator()(Args&&... args) const && 
        -> decltype(!std::invoke(std::move(f), std::forward<Args>(args)...)) {
        return !std::invoke(std::move(f), std::forward<Args>(args)...);
    }
};

Because std::invoke is used, std::not_fn seamlessly handles member object and member function pointers in addition to standard callable entities.

Perfect Forwarding and Value Categories

std::not_fn ensures zero-cost abstraction through rigorous use of perfect forwarding:

  1. Argument Forwarding: Arguments passed to the negated callable wrapper are forwarded to the wrapped target without redundant copying or moving.
  2. Callable Value Categories: Modern implementations provide reference-qualified overloads (&, const &, &&, and const &&). This allows the wrapper to be invoked on rvalues, enabling moves if the underlying callable supports move-only invocations.

Enhancements in C++20

C++20 significantly improved std::not_fn:

  • constexpr Support: The wrapper object returned by std::not_fn can be evaluated at compile time if the underlying callable and its invocation are valid constant expressions.
  • Conditional noexcept: The call operators are marked noexcept whenever the corresponding std::invoke expression combined with logical negation does not throw.

Practical Example

In standard algorithm pipelines, std::not_fn allows existing predicates to be reversed inline without writing boilerplate lambda wrappers:

#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>

bool is_even(int n) {
    return n % 2 == 0;
}

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5, 6};

    // Erase all numbers that are NOT even
    numbers.erase(
        std::remove_if(numbers.begin(), numbers.end(), std::not_fn(is_even)),
        numbers.end()
    );

    // Output: 2 4 6
    for (int n : numbers) {
        std::cout << n << " ";
    }
}

Through type decay, universal references, and standard invocation mechanisms, std::not_fn produces robust, negated function objects with minimal syntax and optimal execution characteristics.