Can I unwrap std::map iterator to structured binding of optionals?

4.4k views Asked by At

Consider following code:

#include<functional>
#include<iostream>
#include<map>

const std::map<int, std::string> numberToStr{{1, "one"}, {2,"two"}};
int main() {
    auto it = numberToStr.find(2);
    if (it ==numberToStr.end()){
        return 1;
    }
    const auto&[_, str] = *it;
    std::cout << str;
}

Is there any way for me to do the unwrapping of potentially dereferenced it to 2 optionals(_ and str) so I can then write:

const auto&[_, str] = // some magic;
// _ is std::optional<int>, str is std::optional<str>
if (!str){
    return 1;
}
std::cout << *str;
}

I presume not since structured bindings are language level thing, and std::optional is a library feature and afaik there is no way to customize the interaction.

Note: I presume I could implement my own map that returns iterators that know if they point to .end(), and "hack" customization points to do optional logic based on that, I am asking for general use case when I do not control the container.

3

There are 3 answers

11
NathanOliver On BEST ANSWER

You could add a helper function like

template <typename Key, typename Value, typename... Rest>
std::pair<std::optional<Key>, std::optional<Value>> my_find(const std::map<Key, Value, Rest...>& map, const Key& to_find)
{
    auto it = map.find(to_find);
    if (it == map.end())
        return {};
    else
        return {it->first, it->second};
}

and then you would use it like

const auto&[_, str] = my_find(numberToStr, 2);
// _ is std::optional<int>, str is std::optional<str>
if (!str){
    return 1;
}
std::cout << *str;

If you only care about the value, you can shorten the code a bit by just returning it instead with

template <typename Key, typename Value, typename... Rest>
std::optional<Value> my_find(const std::map<Key, Value, Rest...>& map, const Key& to_find)
{
    auto it = map.find(to_find);
    if (it == map.end())
        return {};
    else
        return {it->second};
}

and then you'd use it like

auto str = my_find(numberToStr, 2);
// str is std::optional<str>
if (!str){
    return 1;
}
std::cout << *str;
3
ecatmur On

The more C++20-idiomatic route would be to model the iterator as a possibly-empty range:

auto const rng = std::apply(
    [](auto it, auto end) { return std::ranges::subrange(it, end); },
    numberToStr.equal_range(2));
if (rng.empty())
    return 1;
auto const& [_, str] = *rng.begin();
std::cout << str;

Example.

You can do this before C++20 using Boost.Ranges, which has a rather more ergonomic iterator_range:

auto const rng = boost::make_iterator_range(numberToStr.equal_range(2));
// ditto
4
Barry On

The desired API makes little sense to me. Why would you get back two optionals? The key is either in the map or not, that's a single dimension of optionality - it's not like you can get back an engaged key but a disengaged value or a disengaged key but an engaged value.

The API should be:

template <typename Map, typename Key>
auto try_find(Map&, Key&&) -> optional<range_reference_t<Map>>;

But we can't actually write that using std::optional, because that one doesn't support optional references. Returning an actual optional<value_type> is both wasteful (extra copies) and likely semantically invalid (you probably wanted that specific value, not just a value).

So the first step is acquiring a better implementation of optional and using that. At which point, the implementation here is very easy:

template <typename Map, typename Key>
auto try_find(Map& m, Key&& k) -> optional<range_reference_t<Map>>
{
    auto it = m.find(std::forward<Key>(k));
    if (it != m.end()) {
        return *it;
    } else {
        return nullopt;
    }
}

A different approach, which does work with std::optional, is to return an optional iterator instead of an optional reference. This has the benefit of being just as composable as optional while still working entirely within the standard library.


A third approach is to instead return a range:

template <typename Map, typename Key>
auto try_find(Map& m, Key const& k) -> subrange<iterator_t<Map>>
{
    auto [f, l] = m.equal_range(key);
    return subrange(f, l);
}

This continues to be composable with all the ranges things. You just check for emptiness instead of engaged-ness:

auto r = try_find(m, key);
if (r.empty()) {
    // nope
} else {
    // use r.front()
}