Niebloids
Function objects used in std::ranges algorithms that suppress ADL and behave as non-customizable algorithm entry points in C++20.
Niebloidsince C++20A 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.
| Niebloid | CPO | |
|---|---|---|
| User can customize | No | Yes |
| Participates in ADL | No | Partially (to find user customizations) |
| Examples | ranges::sort, ranges::find | ranges::begin, ranges::swap |
Syntax
The canonical implementation skeleton for a niebloid:
// 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 constexprobject β 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. requiresconstraints β C++20 concepts replace SFINAE. Failed constraints produce readable diagnostics.- Projection parameter β
Projtransforms each element before the comparator sees it, a capability absent from the classic API.
Examples
Basic usage and projection syntax
#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:
// 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::findWriting your own niebloid
When building a library algorithm that must interoperate cleanly with ranges code, replicate the pattern rather than writing a function template:
// 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:
// 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; // fineAssuming 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β allstd::ranges::*algorithm names in<algorithm>are niebloids since C++20std::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