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.
Implementing a Custom CPO
A customization point object (CPO) is a function object that dispatches in priority order: member function first, then ADL free function, then a default fallback. This is the pattern used by std::ranges::begin, std::ranges::swap, and every extensible hook in the ranges library.
// C++20 β a reusable "serialize" CPO
namespace mylib {
// Helper: detect member .serialize(out)
template<typename T, typename Out>
concept has_member_serialize = requires(const T& v, Out& out) {
v.serialize(out);
};
// Helper: detect free serialize(v, out) findable by ADL
namespace _adl_serialize {
void serialize() = delete; // poison pill β blocks std::serialize if it existed
template<typename T, typename Out>
concept has_adl_serialize = requires(const T& v, Out& out) {
serialize(v, out); // unqualified: ADL fires here
};
}
struct serialize_fn {
// Priority 1: member function
template<typename T, typename Out>
requires has_member_serialize<T, Out>
constexpr void operator()(const T& v, Out& out) const {
v.serialize(out);
}
// Priority 2: ADL free function (only when no member)
template<typename T, typename Out>
requires (!has_member_serialize<T, Out> &&
_adl_serialize::has_adl_serialize<T, Out>)
constexpr void operator()(const T& v, Out& out) const {
using namespace _adl_serialize;
serialize(v, out); // ADL finds user's overload
}
// Priority 3: default implementation
template<typename T, typename Out>
requires (!has_member_serialize<T, Out> &&
!_adl_serialize::has_adl_serialize<T, Out>)
constexpr void operator()(const T& v, Out& out) const {
// fallback: write a placeholder tag
out << "[unserializable]";
}
};
inline constexpr serialize_fn serialize{};
} // namespace mylibUser types opt in by adding either a member or a free function in their own namespace β the CPO finds the right one automatically:
// Option A: member function (highest priority)
struct Point {
int x, y;
void serialize(std::ostream& out) const { out << x << ' ' << y; }
};
// Option B: free function via ADL (no member needed)
namespace geom {
struct Circle { double r; };
void serialize(const Circle& c, std::ostream& out) { out << "Circle(" << c.r << ')'; }
}
// Both work through the same CPO:
mylib::serialize(Point{1, 2}, std::cout); // calls Point::serialize
mylib::serialize(geom::Circle{3.0}, std::cout); // found via ADL
mylib::serialize(42, std::cout); // fallback: [unserializable]The key properties: callers always write mylib::serialize(...) β one name, deterministic lookup, no Two-Step incantation required.
The Poison Pill Trick
Without a poison pill, the ADL step inside a CPO can accidentally find a standard library function of the same name and pick it up as a "customization" for a type.
What goes wrong without a poison pill:
// Hypothetical CPO without poison pill β DANGEROUS
namespace mylib {
namespace _detail {
// No poison pill here!
template<typename T>
concept has_adl_swap = requires(T& a, T& b) {
swap(a, b); // unqualified lookup
};
}
}
// Problem: for std::vector<int>, ADL finds std::swap β the unconstrained
// C++14 version β which trivially satisfies has_adl_swap for every type.
// The CPO's "user customized" branch fires for ALL types, not just opted-in ones.The fix β a = delete poison pill in the lookup namespace:
namespace mylib::_adl_swap {
// Poison pill: a deleted function template in this namespace.
// Partial ordering means an exact-match user overload beats it,
// but std::swap (found via ADL on std types) is blocked because
// unqualified lookup finds THIS deleted overload first.
template<typename T>
void swap(T&, T&) = delete;
template<typename T>
concept has_adl_swap = requires(T& a, T& b) {
swap(a, b); // now: finds deleted pill OR user's overload; std::swap blocked
};
}The poison pill works because:
- The
= deleted overload lives in the same namespace as the unqualifiedswapcall. - It is found by ordinary unqualified lookup before ADL runs β so
std::swapnever enters the candidate set. - A user's more-specialized free function
swap(MyType&, MyType&)wins over the generictemplate<typename T>pill by partial ordering, so opted-in types still work.
In the standard library, std::ranges::swap uses exactly this technique: a template<class T> void swap(T&&, T&&) = delete; inside the implementation namespace blocks the unconstrained std::swap from polluting the constraint check.
tag_invoke (P1895)
tag_invoke is a proposed (but not yet standardized) alternative CPO mechanism used by stdexec (the reference implementation of C++26 std::execution) and several other modern libraries. Instead of encoding dispatch priority inside each CPO's operator(), it unifies all customization through a single free function named tag_invoke.
The core pattern:
// P1895 tag_invoke pattern (library provides this once)
namespace std {
struct tag_invoke_fn {
template<typename Tag, typename... Args>
constexpr auto operator()(Tag tag, Args&&... args) const
noexcept(noexcept(tag_invoke(tag, std::forward<Args>(args)...)))
-> decltype(tag_invoke(tag, std::forward<Args>(args)...))
{
return tag_invoke(tag, std::forward<Args>(args)...);
}
};
inline constexpr tag_invoke_fn tag_invoke{};
}
// Library author defines a CPO tag type
struct serialize_t {
template<typename T, typename Out>
auto operator()(T&& v, Out& out) const {
return std::tag_invoke(*this, std::forward<T>(v), out);
}
};
inline constexpr serialize_t serialize{};
// User customizes by writing a free tag_invoke overload in their namespace
namespace geom {
struct Circle { double r; };
// First argument is the tag β this is how dispatch is keyed
void tag_invoke(serialize_t, const Circle& c, std::ostream& out) {
out << "Circle(" << c.r << ')';
}
}Niebloid/ADL-CPO vs. tag_invoke β comparison:
| Concern | ADL-based CPO | tag_invoke |
|---|---|---|
| Customization mechanism | Free function findable by ADL in user namespace | tag_invoke overload keyed on the tag type |
| Risk of name collision | Medium β user must avoid clashing names | Very low β tag_invoke is the only reserved name |
| Overload isolation | Separate overload set per CPO name | All CPOs share one overload set, keyed by tag type |
| Discoverability | Easy β grep for the function name | Harder β must know to look for tag_invoke |
| Default implementations | Each CPO manages its own fallback | Same β handled inside the tag type's operator() |
| Standardized | Yes (C++20 CPOs) | No β P1895 not adopted; used in stdexec |
tag_invoke solves the "global name pollution" problem: because all customizations go through the single name tag_invoke, libraries never compete for names like serialize or swap in the global namespace. The tag type itself is the discriminator.
std::ranges CPOs vs. std Algorithms
The std::ranges versions of algorithms are niebloids (ADL-suppressed), while the std versions are function templates (ADL-enabled). This matters when iterators have std as an associated namespace:
std algorithm (function template) | std::ranges equivalent (niebloid) | Key difference |
|---|---|---|
std::sort(first, last) | std::ranges::sort(r) | Ranges version accepts heterogeneous sentinels and projections |
std::find(first, last, v) | std::ranges::find(r, v) | Ranges version returns the iterator; result usable without re-scanning |
std::copy(first, last, out) | std::ranges::copy(r, out) | Ranges version returns in_out_result{in_end, out_end} β no sentinel loss |
std::transform(f,l, out, fn) | std::ranges::transform(r, out, fn) | Ranges version supports projection parameter |
std::for_each(first, last, fn) | std::ranges::for_each(r, fn) | Ranges version returns {in_end, fn} so the function object can be retrieved |
std::min_element(first, last) | std::ranges::min_element(r) | Ranges version returns borrowed_iterator_t β dangling-safe |
std::sort is a function template: using namespace std; sort(v.begin(), v.end()) can accidentally dispatch to a user-namespace sort found via ADL if the iterator type is from that namespace. std::ranges::sort is an object β ADL never fires, and using namespace std::ranges; sort(v) always calls the ranges version.
When to Use Niebloids and CPOs in Your Own Library
Use a CPO (customizable, ADL-dispatching function object) when:
- Your library provides an extensibility hook β callers need to specialize behavior for their types.
- Examples: serialization, hashing, formatting, comparison, resource acquisition.
- The correct pattern is member-first, then ADL free function, then default (as shown above).
Use a niebloid (non-customizable, ADL-suppressing function object) when:
- You are providing an algorithm β the implementation is fixed, and you want to suppress ADL interference.
- You want
using namespace mylib; algo(...)to behave identically tomylib::algo(...). - Your library ships alongside other libraries that may define identically-named algorithms.
Use virtual dispatch instead when:
- Runtime polymorphism is needed β the concrete type is not known at compile time.
- The overhead of a vtable call is acceptable (or negligible) compared to the work done.
Use concepts constraints alone when:
- You want to restrict what types can call an operation, but not redirect to user-provided implementations.
- No customization is needed β the algorithm is closed and correct for any type satisfying the concept.
Use tag_invoke when:
- You are building a library ecosystem where many independent CPOs must coexist without name collisions (e.g., an execution framework like stdexec).
- You want all customizations to be greppable from one central overload set.
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