map
,
unordered_map
, and
flat_map| Document #: | P3091R6 [Latest] [Status] |
| Date: | 2026-06-11 14:14 CEST |
| Project: | Programming Language C++ |
| Audience: |
LWG |
| Reply-to: |
Pablo Halpern <phalpern@halpernwightsoftware.com> |
The most convenient way to look up an element of a
map
-like container is to use the index operator, i.e.,
theMap[key]
. This operator cannot be used, however, when 1) the container is
const
, 2) the mapped type is not default constructible, 3) the
default-constructed value is inappropriate for the context, or 4) the
container should not be modified. These limitations often force the user
to resort to the
find
member function, which returns an iterator that points to a
pair
and typically leads to more complex code having at least one
if
statement and/or duplicate lookup operations. Taking inspiration from
other languages, especially Python, this paper proposes the addition (in
C++29) of a
lookup
member function that returns
optional<T&>
and leverages the observers and monadic operations of
optional
, such as
value_or
, to simplify lookups for
map
,
unordered_map
, and
flat_map
. This proposal’s usefulness would also be enhanced by the
maybe facilities proposed in [P1255].
R6 (2026-06 during the Brno meeting)
get
to
lookup
per 2026-06-10 discussion of P4139 in Brno.noexcept
, as the comparison functor might throw. Conditional
noexcept
was rejected as being unnecessary and overly complicated.contains(x)
” instead of “
find(x) == end
is
false
”.R5 (2025-10 pre-Kona mailing)
unordered_map::get
.R4 (2025-06-20 in Sofia)
constexpr
.flat_map
support..get
and not change it to
.lookup
.R3 (2024-10-15 pre-Wroclaw mailing)
optional<T>::value_or
that will be handled in a separate paper, [P1255], by Steve Downey.R2 (2024-05-23 pre-St. Louis mailing)
value_or
should return an rvalue for both
optional<T>
and
optional<T&>
. This change is reflected in the wording for this paper. Rather than a
separate
optional<T>::or_construct
member, the functionality of
or_construct
is folded into an enhanced
value_or
both
optional<T>
and
optional<T&>
.R1 (post 2024-02-27 LEWGI teleconference)
get
was changed from
mapped_type
to
optional<mapped_type&>
, and the functionality of
get_ref
and
get_as
were delegated to
optional
. Note that
optional<T&>
is not valid in C++23 but is proposed in [P2988R4] for C++26.or_construct
member to
optional
optional<T&>::value_or
over that in [P2988R4]R0 (2024-02-15 pre-Tokyo mailing)
Just about every modern computer language has, as part of the
language or its standard library, one or more associative containers
that uniquely map a key to a value, variously called
map
,
hash_map
,
dictionary
, or something similar. In C++26, we have
std::map
,
std::unordered_map
, and
std::flat_map
. The easy-to-write and easy-to-read expression for retrieving a value
for an associated key is simply
theMap[key]
; in other words, a mapped value is retrieved (and often set) using the
index operator, which returns a reference to the value associated with
the key. Unfortunately, the index operator in the C++ associative
containers has a number of shortcomings compared to many other
languages.
const
containers.Consider a
std::unordered_map
named
theMap
that maps an integer key to a floating-point value, modeling a sparse
array of
double
. If we want to find the largest of the
double
values mapped to the integer keys in the range 1 to 100, we might be
tempted to write the following loop:
double largest = -std::numeric_limits<double>::infinity();
for (int i = 1; i <= 100; ++i)
largest = std::max(largest, theMap[i]);If
theMap
is
const
, the loop will not compile. If any of the keys in the range
[1, 100]
are absent from the map, then
theMap[i]
will return a default-constructed
double
having value 0.0, which might or might not be desirable, depending on
whether we want to treat missing values as truly having value 0.0 or to
ignore them (or, equivalently, to treat them as having value \(-\infty\)). Finally if, before executing
this loop,
theMap
contains only, say, five entries, then at the end of the loop, it will
contain at least 100 entries, most of whose values will be zero.
There are alternatives that avoid all these shortcomings but are
significantly less elegant and therefore more error prone. For example,
the
at
member function looks a lot like
operator[]
and has none of the above shortcomings, but missing keys are handled by
throwing exceptions, making this option impractical for situations other
than when the key is almost certain to exist. Moreover, a
try
-
catch
block within a loop is rarely a clean way to structure code:
double largest = -std::numeric_limits<double>::infinity();
for (int i = 1; i <= 100; ++i)
{
try {
largest = std::max(largest, theMap.at(i));
} catch (const std::out_of_range&) { }
}The above code would work with a
const
map, would ignore missing elements (rather than treating them as zeros),
and would not populate the map with useless entries, but many
programmers would argue that the loop is inelegant at best. In most C++
implementations, this code would be extremely inefficient unless key
misses are rare.
The other obvious alternative uses the
find
member function:
double largest = -std::numeric_limits<double>::infinity();
for (int i = 1; i <= 100; ++i)
{
auto iter = theMap.find(i);
if (iter != theMap.end())
largest = std::max(largest, iter->second);
}This version of the loop is only slightly more verbose than our
starting point and avoids all the issues, but using iterators, needing
to call two member functions (
find
and
end
), and having to extract the
second
member of the element
pair
for what should be a simple operation increases the cognitive
load on both the programmer and the reader. Moreover, a generic use of
find
can yield a subtle bug. Consider the following function template,
f
:
template <class Key, class Value>
void f(const Key& k, const std::map<Key, Value>& aMap)
{
Value obj = some-default-obj-value-expression;
auto iter = aMap.find(k);
if (iter != aMap.end())
obj = iter->second;
// code that uses `obj` ...
}An instantiation of
f
will not compile unless
Value
is copy assignable. Worse, unless tested with a nonassignable type, the
bug could go undetected for a long time. One fix would be initialize
obj
in a single expression:
Value obj = aMap.count(k) ? aMap.at(k) : some-default-obj-value-expression;While correct, this solution involves two lookup operations: one for
the
count
and one for the
at
. A better fix requires a bit more code:
auto iter = aMap.find(k);
Value obj = iter != aMap.end() ? iter->second : some-default-obj-value-expression;Note that the last solution again involves
iterator
,
pair
, and a conditional expression, which is a far cry from the simplicity
of
operator[]
.
Let’s contrast these less-than-ideal map lookups to dictionary lookups in another language and consider one way to write the largest-value computation in Python:
inf = float("inf")
largest = -inf
for i in range(1, 101):
largest = max(largest, theMap.get(i, -inf))The
get
member of Python’s dictionary type looks up the supplied key (first
argument). If the key exists in the dictionary,
get
returns the corresponding value; otherwise,
get
returns the specified alternative value (second argument).
In this paper, I propose
lookup
member functions for
std::map
,
std::unordered_map
, and
std::flat_map
similar to the
get
member in Python dictionaries. Because C++ does not have a
None
value like Python’s,
get
returns an
optional
, instead, and delegates the construction of an alternative return value
to the
optional
observer members.
What’s desired is a simple expression that, given a key, returns the
mapped value if the key exists in a specific map and a user-supplied
alternative value if the key does not exist. The proposed
feature is a
lookup
member function for
map
-like containers that returns
optional<T&>
(or, for a
const
map,
optional<const T&>
):
template<class Key, class T, class Compare = less<Key>,
class Allocator = allocator<pair<const Key, T>>>
class map {
// ...
constexpr optional< mapped_type&> lookup(const key_type& k);
constexpr optional<const mapped_type&> lookup(const key_type& k) const;
//...
};These functions depend on having an
optional
template that can be instantiated with reference types, as proposed in
[P2988R12], which was voted into the
working draft at the June 2025 meeting in Sofia.
Using this feature, the earlier example could be written almost as simply as the Python version:
constexpr double inf = std::numeric_limits<double>::infinity();
double largest = -inf;
for (int i = 1; i <= 100; ++i)
largest = std::max(largest, theMap.lookup(i).value_or(-inf));optional::value_or
To maximize the usefulness and convenience of the proposed
lookup
function, earlier revisions of this paper also proposed enhancements to
optional<T>::value_or
and
optional<T&>::value_or
, allowing (but not requiring) the caller to specify a return type other
than
T
, and giving it a variadic argument list comprising constructor
arguments for the return value. Although such an extension would be
useful, it can also be provided through a non-member function that is
expected to be in the next revision of [P1255] by Steve Downey. Since that
change does not directly relate to the subject of this proposal, there
is no reason to combine it with this paper. You can see the proposed
extensions in a previous revision of this paper, [P3091R2].
The following table shows how operations are simplified using the
proposed new member functions. In these examples,
K
,
T
, and
U
are object types;
m
is an object of (possibly
const
)
std::map<K, T>
,
unordered_map<K, T>
, or
flat_map<K, T>
;
k
is a value compatible with
K
;
v
is an lvalue of type (possibly
const
)
T
; and
a1..aN
are arguments that can used to construct an object of type
T
.
Before
|
After
|
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lookup
The previous version of this paper used the name
map::<K,V>::get
rather than
map<K,V>::lookup
.
The name
get
was borrowed from the Python dictionary member of the same name. Other
names considered were
try_at
,
lookup_at
,
get_optional
, and
lookup_optional
. The
get
name was originally selected for brevity and consistency with other
languages, but is different from all other uses of
get
in the Standard Library in that no other use of
get
returns without retrieving the requested value (among other
inconsistencies described in [P4139R3]). LEWG changed the name to
lookup
following discussion of [P4139R2] in Brno on 2026-06-10:
POLL: Forward “P4139R2: Better Name for Better Lookups in P3091” with the name
lookupand without the addition of the random access sequence container (section 4.3) to LWG for C++29.
SF
|
F
|
N
|
A
|
SA
|
|---|---|---|---|---|
| 5 | 12 | 5 | 4 | 2 |
[P4139R2] additionally proposed that accessing a random-access sequential container using an index that might be out of bounds is similar to accessing an associative container with a key that might not exist, and thus proposed that a function having the same name be added to such random-access containers. This proposal failed to reach consensus in LEWG in Brno, however, and was removed from [P4139R3].
map
Version R0 of this paper proposed
get
, and
get_ref
member functions that would look up a key and return the corresponding
value (or a reference to the corresponding value) or else construct an
alternative from the nonkey arguments without, involving
optional<T&>
:
// return a value
template <class U = remove_cvref_t<T>, class... Args>
U get(const key_type& key, Args&&... args);
template <class U = remove_cvref_t<T>, class... Args>
U get(const key_type& key, Args&&... args) const;
// return a reference
template <class Arg>
auto get_ref(const key_type& key, Arg&& ref) -> common_reference_t< mapped_type&, Arg>;
template <class Arg>
auto get_ref(const key_type& key, Arg&& ref) const -> common_reference_t<const mapped_type&, Arg>;The following table shows the usage of the R0 design compared to the currently proposed design.
R0 Design (
get
Returns
T
)
|
Current Design (
lookup
Returns
optional<T&>
)
|
|---|---|
|
|
|
|
|
|
|
|
|
|
Advantages of the R0 Design Over the Current Design
get
(and
get_ref
) in many cases.Advantages of the Current Design Over the R0 Design
lookup
provides a direct indication, via a disengaged
optional
return value, that the key was not found.optional
, the current design can leverage all of the functionality of
optional
, including
value_or
and monadic operations. Any future improvements to
optional
could be accessed by users of
map::lookup
without modifying
map
. In particular, all of the features in [P1255] could be applied to the return
value of
map::lookup
, including a
std::reference_or
function.lookup
) compared to two observers for the R0 design (
get
and
get_ref
). Since each observer requires four overloads (
const
and non-
const
, each having a
key_type
or templated
K
parameter), the interface simplification is notable.lookup
is simple to specify and implement, making
lookup
easy to add to nonstandard map-like containers and new standard
containers such as
flat_map
and even for random-access sequence containers, including
vector
and
deque
, should the committee choose to do that.During the 2024-02-27
LEWGI (SG18) telecon, unanimous consent was achieved for
lookup
returning
optional<T&>
(known as the Alternative Design in [P3091R0]):
POLL: The alternative design with a smaller container API with extensions to
std::optionalshould be pursued.
SF
|
WF
|
N
|
WA
|
SA
|
|---|---|---|---|---|
| 6 | 4 | 0 | 0 | 0 |
Providing the functionality described in this paper is possible using
namespace-scope functions, without modifying
std::map
and
std::unordered_map
:
template <class Map, class K, class... Args>
typename optional<typename Map::mapped_type&> lookup(Map& m, const K& k);
template <class Map, class K, class... Args>
typename optional<const typename Map::mapped_type&> lookup(const Map& m, const K& k);
auto x = lookup(m, k).value_or(v);One benefit to this approach is that the namespace-scoped
lookup
template can handle any map-like dictionary type (i.e., a type
having a
mapped_type
and a
find
method that returns an
iterator
pointing to a key-value
pair
). Such an approach, however, has disadvantages.
lookup
outside of the map interface.An implementation, with tests and usage examples, can be found at https://github.com/phalpern/WG21-halpern/tree/main/P3091-map_lookup/code.
Some of the functionality can be found in Meta’s [Folly] library.
This wording is relative to the July 2025 working draft, [N5014].
To the list in 17.3.2 [version.syn]1, add:
#define __cpp_lib_map_lookup yyyymmL // also in <map>, <unordered_map>, <flat_map>
std::map
ChangesIn 23.4.3.1
[map.overview]/2,
insert the
lookup
element-access members:
//23.4.3.3 [map.access], element access
constexpr mapped_type& operator[](const key_type& x);
constexpr mapped_type& operator[](key_type&& x);
template<class K> constexpr mapped_type& operator[](K&& x);
constexpr mapped_type& at(const key_type& x);
const constexpr mapped_type& at(const key_type& x) const;
template<class K> constexpr mapped_type& at(const K& x);
template<class K> const constexpr mapped_type& at(const K& x) const;constexpr optional<mapped_type&> lookup(const key_type& x);
constexpr optional<const mapped_type&> lookup(const key_type& x) const;
template<class K> constexpr optional<mapped_type&> lookup(const K& x);
template<class K> constexpr optional<const mapped_type&> lookup(const K& x) const;
At the end of 23.4.3.3 [map.access], add these descriptions:
constexpr optional<mapped_type&> lookup(const key_type& x);
constexpr optional<const mapped_type&> lookup(const key_type& x) const;
template<class K> constexpr optional<mapped_type&> lookup(const K& x);
template<class K> constexpr optional<const mapped_type&> lookup(const K& x) const;
Constraints: For the third and fourth overloads, the qualified-id
Compare::is_transparentis valid and denotes a type.
Preconditions: The expression
find(x)is well-formed and has well-defined behavior.
Returns:
find(x)->secondifcontains(x)istrue, otherwisenullopt.
Complexity: Logarithmic.
std::unordered_map
ChangesIn 23.5.3.1
[unord.map.overview]/3,
insert the
lookup
element-access members:
//23.5.3.3 [unord.map.elem], element access
constexpr mapped_type& operator[](const key_type& x);
constexpr mapped_type& operator[](key_type&& x);
template<class K> constexpr mapped_type& operator[](K&& x);
constexpr mapped_type& at(const key_type& x);
const constexpr mapped_type& at(const key_type& x) const;
template<class K> constexpr mapped_type& at(const K& x);
template<class K> const constexpr mapped_type& at(const K& x) const;constexpr optional<mapped_type&> lookup(const key_type& x);
constexpr optional<const mapped_type&> lookup(const key_type& x) const;
template<class K> constexpr optional<mapped_type&> lookup(const K& x);
template<class K> constexpr optional<const mapped_type&> lookup(const K& x) const;
At the end of 23.5.3.3 [unord.map.elem], add these descriptions:
constexpr optional<mapped_type&> lookup(const key_type& x);
constexpr optional<const mapped_type&> lookup(const key_type& x) const;
template<class K> constexpr optional<mapped_type&> lookup(const K& x);
template<class K> constexpr optional<const mapped_type&> lookup(const K& x) const;
Constraints: For the third and fourth overloads, the qualified-ids
Hash::is_transparentandPred::is_transparentare valid and denote types.
Preconditions: The expression
find(x)is well-formed and has well-defined behavior.
Returns:
find(x)->secondifcontains(x)istrue, otherwisenullopt.
Complexity: Average case constant, worst case linear in
size().
Editorial Note to LWG: The Complexity clause is technically redundant because the return value is specified in terms of
find. I added it here to match the format ofmap::lookup, which in turn matches the format ofmap::at. However, Complexity is currently missing for two of the overloads ofunordered_map::atthat are not specified in terms iffind. Should that be an LWG issue?
std::flat_map
ChangesIn 23.6.8.2
[flat.map.defn],
insert the
lookup
element-access members:
//23.6.8.6 [flat.map.access], element access
constexpr mapped_type& operator[](const key_type& x);
constexpr mapped_type& operator[](key_type&& x);
template<class K> constexpr mapped_type& operator[](K&& x);
constexpr mapped_type& at(const key_type& x);
constexpr const mapped_type& at(const key_type& x) const;
template<class K> constexpr mapped_type& at(const K& x);
template<class K> constexpr const mapped_type& at(const K& x) const;constexpr optional<mapped_type&> lookup(const key_type& x);
constexpr optional<const mapped_type&> lookup(const key_type& x) const;
template<class K> constexpr optional<mapped_type&> lookup(const K& x);
template<class K> constexpr optional<const mapped_type&> lookup(const K& x) const;
At the end of 23.6.8.6 [flat.map.access], add these descriptions:
Editorial Note to LWG: The following description is
character-for-character identical to that of
map::lookup
. The same is true for the existing descriptions of
operator[]
and
at
. Consider moving all of these descriptions into 23.2.7.1
[associative.reqmts.general]
and, for consistency, move the
unordered_map
versions into 23.2.8.1
[unord.req.general].
Resolution: LWG agrees, but there should be a separate
paper, as there is a sizable set of similar functions for which this
change should be made.
constexpr optional<mapped_type&> lookup(const key_type& x);
constexpr optional<const mapped_type&> lookup(const key_type& x) const;
template<class K> constexpr optional<mapped_type&> lookup(const K& x);
template<class K> constexpr optional<const mapped_type&> lookup(const K& x) const;
Constraints: For the third and fourth overloads, the qualified-id
Compare::is_transparentis valid and denotes a type.
Preconditions: The expression
find(x)is well-formed and has well-defined behavior.
Returns:
find(x)->secondifcontains(x)istrue, otherwisenullopt.
Complexity: Logarithmic.
Thanks to Tomasz Kamiński for pushing me on the
optional<T&>
approach.
Thanks to Steve Downey for working with me to harmonize P2988 and P1255 with this paper.
Thanks to Lori Hughes for editing support.
views::nullable
And a concept to constrain
maybes. All citations to the Standard are to working draft N5014 unless otherwise specified.↩︎