std::less in C++: Modern Patterns and Practical Examples

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

#include

#include

int main() {

// Transparent comparator enables heterogeneous lookup in many standard libraries.

std::map<std::string, int, std::less> httpStatus {

{"ok", 200},

{"not_found", 404},

{"toomanyrequests", 429},

};

std::stringview key = "notfound";

auto it = httpStatus.find(key);

if (it != httpStatus.end()) {

std::cout <first < " <second << '\n';

}

}

Practical payoff: fewer allocations and fewer temporary objects in hot paths. In a service that parses thousands of headers per second, those small savings add up.

If you use std::less, the container may require a std::string for lookup keys, which pushes you toward allocations.

Example 5: lower_bound with the same ordering rule

Binary search style algorithms assume the range is sorted using the same comparator you use for the search.

#include

#include

#include

#include

int main() {

std::vector latenciesMs {3, 5, 8, 8, 13, 21, 34};

int target = 10;

auto it = std::lower_bound(

latenciesMs.begin(),

latenciesMs.end(),

target,

std::less{}

);

if (it != latenciesMs.end()) {

std::cout <= " << target << " is " << *it << '\n';

} else {

std::cout << "All values < " << target << '\n';

}

}

If you sort descending and then call lower_bound with std::less, you’ll get wrong results. I’ve seen this happen in production more than once because someone “just flipped sort order” and forgot to update the binary search.

Rule I follow: store the comparator next to the container/range (as a type alias or a constexpr object) so you can’t accidentally mismatch.

Strict weak ordering: the rule behind sane algorithms

STL algorithms like std::sort and containers like std::set require that your comparator define a strict weak ordering. That phrase can sound academic, but it maps to practical expectations:

  • Irreflexive: comp(x, x) is always false.
  • Transitive: if comp(a, b) and comp(b, c) then comp(a, c).
  • “Equivalence” is consistent: if neither a<b nor b<a, then a and b are equivalent for ordering purposes.

If you break these rules, algorithms can misbehave in ways that look random: infinite loops, corrupted sets, missing keys, or unstable results across builds.

A common trap: floating-point NaNs

With IEEE floats, comparisons involving NaN return false for both < and >.

That means std::less{}(nan, 1.0) is false and std::less{}(1.0, nan) is also false. So NaN becomes “equivalent” to everything, which is not a real equivalence relation for ordering.

In a sort, that can violate the comparator requirements. Don’t “hope for the best.” If your data can contain NaNs, define an explicit policy.

Example 6: A NaN-safe ordering for doubles

Here’s a comparator that pushes NaNs to the end, and otherwise sorts ascending:

#include

#include

#include

#include

#include

struct NanLastAscending {

bool operator()(double a, double b) const {

const bool aNaN = std::isnan(a);

const bool bNaN = std::isnan(b);

if (aNaN != bNaN) return bNaN; // a < b when b is NaN? false; when a is NaN? true

if (aNaN && bNaN) return false; // treat NaNs as equivalent

return std::less{}(a, b);

}

};

int main() {

std::vector metrics {3.0, std::nan(""), 2.0, 5.0, std::nan(""), 4.0};

std::sort(metrics.begin(), metrics.end(), NanLastAscending{});

for (double x : metrics) {

if (std::isnan(x)) std::cout << "NaN ";

else std::cout << x << ' ';

}

std::cout << '\n';

}

This is the kind of policy I like to encode once and reuse. If your domain says “NaN means missing,” then pushing NaNs to the end is often what you want.

Pointers and std::less: stable ordering when < is shaky

Pointer ordering is a surprisingly sharp corner.

For pointers into the same array/object, < has a well-defined meaning (it compares addresses within that array). For unrelated objects, the meaning of < on raw pointers has historically been problematic; code that assumes a total order across unrelated pointers is risky.

The standard library addresses this by defining pointer special handling for std::less so that you can still build ordered containers keyed by pointers with a consistent ordering.

Example 7: Sorting pointers for deterministic output

Sometimes you want deterministic output for debugging or logging. std::less gives you a consistent ordering for pointers.

#include

#include

#include

#include

int main() {

int a = 10;

int b = 20;

int c = 30;

std::vector refs {&b, &c, &a};

std::sort(refs.begin(), refs.end(), std::less{});

for (const int* p : refs) {

std::cout << p < " << *p << '\n';

}

}

In production code, I rarely want to “sort by address” as a business rule, but I do want deterministic ordering for diagnostics and reproducible outputs. When you do, std::less is the right standard tool.

If you actually want to sort objects by their values, sort by the pointed-to value instead:

// conceptually: compare p and q

Don’t conflate “pointer identity” with “object order.”

Custom types: designing ordering that won’t bite you later

If you author a type that will be sorted or used as a key, you need an ordering story. My approach is:

  • Decide what ordering means in your domain.
  • Make it explicit (either as operator< or as a named comparator).
  • Prefer comparisons that are easy to keep consistent as the type evolves.

Example 8: A key type for ordered containers

Let’s say you model a build artifact with a semantic version and a package name. A stable ordering might be: name ascending, then version ascending.

#include

#include

#include

#include

#include

struct Version {

int major = 0;

int minor = 0;

int patch = 0;

// Spaceship makes ordering easier to keep correct.

auto operator(const Version&) const = default;

};

struct PackageKey {

std::string name;

Version version;

// Define ordering in one place.

auto operator(const PackageKey&) const = default;

};

int main() {

std::map<PackageKey, std::string, std::less> artifacts;

artifacts[{"renderer", {1, 4, 2}}] = "renderer-1.4.2.tgz";

artifacts[{"renderer", {1, 5, 0}}] = "renderer-1.5.0.tgz";

artifacts[{"parser", {2, 0, 1}}] = "parser-2.0.1.tgz";

for (const auto& [key, file] : artifacts) {

std::cout << key.name << " "

<< key.version.major << "." << key.version.minor << "." << key.version.patch
< " << file << '\n';

}

}

Notes I care about:

  • The comparator is std::less, but the ordering actually comes from the type’s comparison operators.
  • Using operator (spaceship) reduces the risk of inconsistent manual comparisons.
  • The ordering is lexicographic across fields, which is easy to explain and stable over time.

If you can’t or don’t want to define ordering on the type itself (for example, multiple orderings are valid), write a named comparator type and store it next to the type definition.

Common mistakes I see (and how I avoid them)

1) Including the wrong header

I consistently use . If you rely on , you’re leaning on non-portable, legacy behavior.

2) Mismatched sort/search comparators

If you sort with one comparator and binary search with another, you’ll get wrong answers. I keep the comparator as a named constant or a type alias.

Example pattern:

using Ordering = std::less;

constexpr Ordering ordering{};

Then I pass ordering everywhere.

3) Assuming std::less “fixes” NaNs

It doesn’t. If your data contains NaNs, write an explicit NaN policy comparator.

4) Writing comparators that aren’t strict

A frequent bug is using <= instead of <:

// Wrong for ordering: returns true for equal values

return a.value <= b.value;

That breaks irreflexivity and can corrupt ordered containers.

5) Capturing state in a comparator without thinking

Stateful comparators (capturing lambdas) can be valid for algorithms, but they are tricky for associative containers because the comparator becomes part of the container’s type and invariants. When I need state, I make it explicit and ensure it is immutable for the container’s lifetime.

6) Comparing strings by pointer identity

I still see code that sorts const char using std::less<const char>, expecting lexicographic order. That sorts by address, not by text. If you want lexicographic ordering, compare std::string_view or use std::strcmp-style comparisons carefully.

Practical takeaways you can apply this week

When you’re writing C++ that will live longer than a weekend, comparisons deserve the same care as memory ownership and error handling. I treat std::less as a small but high-signal tool: it encodes intent, keeps generic code readable, and helps you standardize ordering policies across algorithms and containers.

Here’s the checklist I actually use. If you store keys in ordered containers, choose std::less by default so you can add heterogeneous lookup later without redesigning the API. If you sort and then binary-search, keep the comparator in one named place and reuse it everywhere. If your values can include NaNs or other “not totally ordered” cases, write the policy comparator once and make it obvious (NaNs last, NaNs first, or NaNs rejected).

For modern C++ workflows in 2026, I also recommend wiring comparator correctness into your tooling: run clang-tidy rules that catch suspicious comparisons, enable sanitizers in debug builds, and add small unit tests around key ordering rules (especially for types used as std::map keys). You won’t measure that as “milliseconds saved,” but you will feel it as fewer heisenbugs and fewer mysterious production-only mismatches.

If you want a next step that pays off fast, pick one frequently used ordered container in your codebase and switch its comparator to std::less. Then add one heterogeneous lookup call (like finding a std::string key using std::string_view). That single change tends to clarify intent, reduce allocations in hot paths, and make future refactors safer.

Scroll to Top