Skip to content
C++
Idiom
since C++20
Intermediate

Niebloids

Function objects used in std::ranges algorithms that suppress ADL and behave as non-customizable algorithm entry points in C++20.

Niebloidsince C++20

A niebloid is an inline constexpr function object whose operator() implements a standard algorithm, preventing ADL from finding it and replacing the implicit swap/iter_swap lookup used by classic <algorithm> entries.

Overview

The C++20 ranges library rewrites the standard algorithms β€” std::ranges::sort, std::ranges::find, std::ranges::transform, and the rest β€” not as function templates but as callable objects of uniquely-named struct types. The design was championed by Eric Niebler (hence the informal name) and resolves two persistent problems with the classic <algorithm> API.

Problem 1: Unintended ADL. Classic std::sort is a function template. Function templates participate in ADL, which means a sort in a user namespace can be found accidentally, or explicit using std::sort can trigger lookup collisions that produce hard-to-diagnose overload ambiguity. Niebloids, being function objects rather than functions, are never found by ADL β€” the call site always resolves to exactly the object named.

Problem 2: Implicit swap coupling. std::sort (C++98) uses ADL swap to move elements. If a type provides a namespace-scoped swap, it gets picked up silently. std::ranges::sort, as a niebloid, uses std::ranges::iter_swap instead β€” a customization point object with explicit, priority-ordered lookup. This makes the coupling visible and deliberate rather than implicit.

The standard mandates in [algorithm.requirements] that std::ranges algorithm names have the semantics of niebloids: they are not function templates, they do not participate in ADL, and they cannot be found by unqualified or argument-dependent lookup even if a using-declaration brings them into scope.

Niebloids vs. Customization Point Objects

A related but distinct pattern is the customization point object (CPO): std::ranges::begin, std::ranges::end, std::ranges::swap. CPOs are also inline constexpr function objects, but their job is to provide a customizable hook β€” user types opt in by providing specific overloads the CPO will find. Niebloids are the opposite: non-customizable algorithm implementations that intentionally block user-namespace interference.

NiebloidCPO
User can customizeNoYes
Participates in ADLNoPartially (to find user customizations)
Examplesranges::sort, ranges::findranges::begin, ranges::swap

Syntax

The canonical implementation skeleton for a niebloid:

cpp
// C++20
namespace std::ranges {

    struct sort_fn {
        template<
            std::random_access_iterator I,
            std::sentinel_for<I> S,            // heterogeneous sentinel β€” C++20
            class Comp = ranges::less,
            class Proj = std::identity         // projection parameter β€” C++20
        >
        requires std::sortable<I, Comp, Proj>
        constexpr I
        operator()(I first, S last,
                   Comp comp = {}, Proj proj = {}) const;

        template<
            ranges::random_access_range R,
            class Comp = ranges::less,
            class Proj = std::identity
        >
        requires std::sortable<ranges::iterator_t<R>, Comp, Proj>
        constexpr ranges::borrowed_iterator_t<R>
        operator()(R&& r, Comp comp = {}, Proj proj = {}) const;
    };

    inline constexpr sort_fn sort{};  // the niebloid β€” not a function
}

Key elements:

  • inline constexpr object β€” not a function, not a function template. Taking its address is ill-formed.
  • Templated operator() const β€” template arguments are deduced from call-site types; the struct itself is not a template.
  • requires constraints β€” C++20 concepts replace SFINAE. Failed constraints produce readable diagnostics.
  • Projection parameter β€” Proj transforms each element before the comparator sees it, a capability absent from the classic API.

Examples

Basic usage and projection syntax

cpp
#include <algorithm>
#include <vector>
#include <string>
// C++20

struct Employee {
    std::string name;
    double salary;
};

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

    std::ranges::sort(nums);                          // ascending
    std::ranges::sort(nums, std::ranges::greater{});  // descending

    std::vector<Employee> staff{
        {"Alice", 95000}, {"Bob", 72000}, {"Carol", 110000}
    };

    // C++17 style β€” comparator must unpack both sides
    std::sort(staff.begin(), staff.end(),
        [](const Employee& a, const Employee& b){ return a.salary < b.salary; });

    // C++20 β€” projection cleanly separates "how to extract" from "how to compare"
    std::ranges::sort(staff, {}, &Employee::salary);

    // multi-key sort: primary by salary desc, secondary by name asc
    std::ranges::sort(staff, std::ranges::greater{}, &Employee::salary);
}

Heterogeneous sentinels

Niebloids accept a sentinel type distinct from the iterator type β€” classic std::find requires both to be the same:

cpp
// C++20
const char* raw = "find the comma, please";

struct comma_sentinel {
    friend bool operator==(const char* p, comma_sentinel) {
        return *p == ',' || *p == '\0';
    }
};

auto it = std::ranges::find(raw, comma_sentinel{}, 'i');
// works with heterogeneous I / S β€” ill-formed with std::find

Writing your own niebloid

When building a library algorithm that must interoperate cleanly with ranges code, replicate the pattern rather than writing a function template:

cpp
// C++20
namespace mylib {

    struct adjacent_transform_fn {
        template<
            std::forward_iterator I,
            std::sentinel_for<I> S,
            std::weakly_incrementable O,
            std::copy_constructible BinaryOp
        >
        requires std::indirectly_writable<
                     O, std::indirect_result_t<BinaryOp&, I, I>>
        constexpr O
        operator()(I first, S last, O out, BinaryOp op) const {
            if (first == last) return out;
            auto prev = first;
            while (++first != last) {
                *out++ = op(*prev, *first);
                prev = first;
            }
            return out;
        }

        template<std::ranges::forward_range R,
                 std::weakly_incrementable O,
                 std::copy_constructible BinaryOp>
        constexpr O
        operator()(R&& r, O out, BinaryOp op) const {
            return (*this)(std::ranges::begin(r), std::ranges::end(r),
                           std::move(out), std::move(op));
        }
    };

    inline constexpr adjacent_transform_fn adjacent_transform{};
}

// Call site β€” fully qualified, no ADL, no surprises
std::vector<int> src{1, 2, 3, 4, 5};
std::vector<int> dst;
mylib::adjacent_transform(src, std::back_inserter(dst), std::plus{});
// dst == {3, 5, 7, 9}

Best Practices

Always qualify the namespace at the call site. Write std::ranges::sort rather than using namespace std::ranges; sort(...). The entire purpose of the design is deterministic, non-ADL lookup β€” don't undermine it with a using directive.

Prefer projections over comparator lambdas. std::ranges::sort(v, {}, &T::key) outclasses a two-argument lambda in readability and composes cleanly when you need to chain transformations through std::views.

Prefer range overloads. std::ranges::sort(v) over std::ranges::sort(v.begin(), v.end()). The range overload handles sentinel extraction, supports borrowed_range tracking, and is less to type.

Implement library algorithms as niebloids, not function templates. Function templates are found by ADL and can be shadowed or create ambiguity with user-namespace names. A function object in your namespace is always fully qualified at the call site.

Common Pitfalls

Taking the address of a ranges algorithm. std::ranges::sort is an object, not a function. Storing it in a function<> or a function pointer fails:

cpp
// ill-formed β€” C++20
auto* p = &std::ranges::sort;     // error: not a function

// correct β€” wrap in a lambda
auto sorter = [](auto&& r){ std::ranges::sort(r); };
std::function<void(std::vector<int>&)> f = sorter; // fine

Assuming ADL extends niebloids. Unlike classic algorithms where using std::sort; sort(v.begin(), v.end()) can be overridden by a user-namespace sort found via ADL, a using-declaration for a niebloid does not open the door to ADL extension. If you need a customization hook, you need a CPO, not a niebloid.

Mixing std::sort and std::ranges::sort swap semantics. std::sort uses ADL swap; std::ranges::sort uses std::ranges::iter_swap. If your type customizes swap in its namespace but does not also satisfy std::swappable via the ranges mechanism, the two algorithms may behave differently. Test with both or commit to one API.

Misreading sentinel mismatch errors. Because niebloids accept sentinel_for<I> S, a type mismatch at the second argument produces a concept-constraint failure rather than a direct type error. The message will mention std::sentinel_for β€” look for the iterator type deduced from the first argument and verify the second argument models sentinel_for that type.

See Also

  • std::ranges β€” all std::ranges::* algorithm names in <algorithm> are niebloids since C++20
  • std::identity β€” the default projection (C++20, <functional>)
  • std::ranges::less, std::ranges::greater β€” comparison objects used as default comparators in ranges algorithms; also niebloid-adjacent in design
  • Customization point objects: std::ranges::begin, std::ranges::end, std::ranges::swap β€” the customizable counterpart to niebloids