A few years ago I reviewed a bug that looked impossible: two different builds of the same C++ service produced slightly different ordering for a set of pointers, and the difference cascaded into mismatched cache keys. The root cause wasn’t a race or UB in the obvious place—it was an implicit comparison policy nobody had written down. Once we replaced “whatever the default is” with an explicit comparator, the behavior became stable and the code became easier to reason about.
That’s the real value of std::less: it’s not fancy syntax. It’s a small, standard comparison object that communicates intent, composes well with the STL, and avoids a bunch of subtle pitfalls when you move from “toy sorting” to real systems: ordered maps/sets, binary searches, heterogeneous lookup, custom types, and yes—pointer comparisons.
You’ll leave with working examples you can paste into a compiler, plus practical rules I use in production: when std::less is the right tool, when std::less is better, what strict-weak-ordering really means in practice, and which edge cases (floating-point NaNs, mixed string types, pointers) deserve extra care.
Why std::less shows up everywhere
When you write a < b, you’re choosing a comparison rule that’s tightly coupled to the types of a and b. That’s fine for one-off code, but the moment you work with the standard library at scale, you start wanting comparisons as first-class values:
- Algorithms take comparators so you can define ordering without rewriting the algorithm.
- Containers like
std::map and std::set build their entire identity around an ordering.
- Generic code wants a default ordering that is conventional, cheap, and predictable.
std::less is the standard “less-than” function object. Think of it like a tiny adapter that turns “the < operator for this type” into a callable you can pass around.
The simple mental model:
std::less{}(x, y) behaves like x < y for T.
- It’s stateless and trivially cheap to copy.
- It plays nicely with templates and overload resolution.
In my experience, the most important reason to reach for std::less isn’t performance—it’s communication. When I see sort(v.begin(), v.end(), std::less{});, I know the author intentionally chose ascending order with the standard comparison rule, not “whatever accidental lambda happened to get typed.”
What std::less actually is (and what it isn’t)
The right header
Use:
#include
You may see older code mention . That header name is not the modern, standard way to include the facility in portable C++.
The core idea
std::less is a function object type. It exposes operator() and returns a bool. A simplified sketch looks like this:
template
struct less {
constexpr bool operator()(const T& x, const T& y) const {
return x < y;
}
};
In real standard library implementations, there are extra details (constexpr, noexcept where possible, transparent specializations, and historically nested typedefs).
What it is not
- It’s not a sorting algorithm.
- It’s not a total ordering for all types.
- It does not magically fix broken comparison operators.
If T has an operator< that violates ordering rules, then std::less will inherit those problems. The library assumes your comparator defines a strict weak ordering (more on that soon).
A useful modern variant: std::less
std::less (note the empty angle brackets) is typically the best default in new code:
- It is “transparent” (supports mixed-type comparisons where that makes sense).
- It’s a better fit for heterogeneous lookup in associative containers.
When I’m writing a container comparator in 2026-era C++, I default to std::less unless I have a good reason to lock the comparator to a single T.
Sorting with std::less: making intent explicit
Sorting is the easiest place to start because you can see behavior immediately.
Example 1: Sorting integers (explicit ascending)
This is intentionally simple, but it demonstrates the core usage: pass std::less{} as the comparator.
#include
#include
#include
#include
static void print(const std::vector& values) {
for (int x : values) std::cout << x << ' ';
std::cout << '\n';
}
int main() {
std::vector scores {26, 23, 21, 22, 28, 27, 25, 24};
std::sort(scores.begin(), scores.end(), std::less{});
print(scores);
}
Why do this when the default std::sort already sorts ascending? Because the moment you refactor to accept a comparator parameter, or you copy/paste and flip ordering, the explicitness pays for itself.
Example 2: Sorting a struct by a field
I prefer writing comparisons as “project then compare” using std::ranges where available, but plenty of codebases still use classic std::sort. Here’s the classic style:
#include
#include
#include
#include
#include
struct BuildJob {
std::string name;
int priority; // higher means more urgent
int estimatedMinutes;
};
int main() {
std::vector queue {
{"unit-tests", 10, 8},
{"lint", 3, 2},
{"package", 5, 4},
{"integration", 7, 15}
};
// Sort by estimated time ascending.
std::sort(queue.begin(), queue.end(),
[](const BuildJob& a, const BuildJob& b) {
return std::less{}(a.estimatedMinutes, b.estimatedMinutes);
}
);
for (const auto& job : queue) {
std::cout << job.name << " (" << job.estimatedMinutes << "m)\n";
}
}
That std::less{} inside the lambda might look redundant, but it’s a pattern I like in generic code: it makes it obvious that ordering is based on the “natural” < of the projected type.
Performance note
Calling std::less is typically inlined and costs the same as using < directly. The algorithm’s cost dominates:
- Sorting is
O(n log n) comparisons.
- Each comparison is
O(1) for most arithmetic types, but may be more expensive for strings or custom types.
If you’re trying to reduce runtime, focus first on reducing comparisons (choose the right algorithm, keep keys small, precompute projections), not on micro-tuning the comparator wrapper.
std::less in generic code: defaults, templates, and std::less
Generic code is where std::less earns its keep. You can express “this algorithm needs an ordering, and the default should be conventional” without tying yourself to a specific type.
Example 3: A templated comparison helper
Here’s a small helper that defaults to std::less, so it works with mixed arithmetic types and many comparable pairs:
#include
#include
template <class A, class B, class Compare = std::less>
bool is_before(const A& a, const B& b, Compare comp = Compare{}) {
return comp(a, b);
}
int main() {
std::cout << std::boolalpha;
int x = 1;
long y = 2;
std::cout << is_before(x, y) << '\n';
x = 2;
y = -1;
std::cout << is_before(x, y) << '\n';
}
Why std::less instead of std::less? Because locking to int is a silent narrowing trap in templated contexts. In 2026, most teams I work with treat std::less as the default unless you’re writing something intentionally type-specific.
Traditional vs modern: comparator choices
Here’s how I think about common approaches in real code:
Approach |
Example
When I pick it |
— |
—
— |
Hard-coded < |
return a < b;
Very small, local code where ordering is obvious and never changes |
Named comparator type |
struct ById { bool operator()(...) const; };
When the ordering is part of a domain concept and needs a name |
Lambda comparator |
[](auto& a, auto& b){...}
Quick one-offs, especially with projections |
std::less |
std::less{}
When the type is fixed and I want explicit intent |
std::less |
std::less{}
Default for generic code and heterogeneous comparisons |
Ranges comparator |
std::ranges::less{}
When using std::ranges algorithms and projections |
If your codebase is already on C++20/23/26, I recommend leaning toward ranges algorithms because the projection parameter often removes the need for custom comparator lambdas.
Ordered containers and heterogeneous lookup: where std::less shines
Associative containers (std::map, std::set, and their multi- variants) use a comparator to define “equivalence” and ordering inside a tree structure.
A critical point: in a std::map, two keys are considered equivalent if neither is less than the other according to Compare. That’s why comparator correctness matters so much.
Example 4: Fast lookups with std::less and std::string_view
Suppose you store std::string keys, but you often look up by std::string_view (for example, parsing a request buffer without allocating). With a transparent comparator, you can often call find without constructing a std::string.
#include
#include
#include