[C++] Exploring the Potential About a std::find Wrapper

In my last article , I started thinking about extending find to append .and_then(..) and or_else(..), as in C++23's std::optional. Let's break this down into some considerations in this article.

I did some minor changes, but the basic principle remains like the last time. To summarize this briefly, I was trying to have a mechanism to retrieve an element from a container and then append a .and_then(..) and a or_else(..) function. I created a type find_result and wrapped std::find in my own find:

    template<typename T> 
    class find_result 
    {
        public: 
            // and_then: only calls the passed function if the element was found
            template<typename Func>
            const find_result<T>& and_then(const Func&& func) const;

            // or_else: calls the lambda when no element was found
            template<typename Func>
            void or_else(const Func&& func) const;

        private: 
        // ...  
    };
    // ... 
// cwt::find just wrapps std::find and creates my find_result
cwt::find(some_vector, some_value)
    .and_then([](const std::size_t* found_value){/* do this*/})
    .or_else([](){/*do that*/});

Sequence Containers, like std::vector

I don't think this approach makes much sense for sequential containers, because there's just another way to write code, and I'm not saving lines of code:

// we'd call define 
// and write down two lambdas for
// both cases, value found or not found
cwt::find(some_vector, some_value)
    .and_then([](const std::size_t* found_value){ 
        // do this
    })
    .or_else([](){ 
        // do that
    });

// which is with the iterator type almost the same 
auto it = std::find(some_vector.begin(), some_vector.end(), some_value);
if (it != v.end()) {
    // do this 
} else {
    // do that
}

For me, this is just another style without much advantage.

Key-Value Pairs, like std::unordered_map

But let's take a closer look if we can make use of this on a map. I created a wrapper for find_if:

// our final type which we return
template<template<typename...> typename Container, typename Key, typename Value>
using Result = details::find_result<typename Container<Key,Value>::value_type*>;

template<template<typename...> typename Container, typename Key, typename Value, typename Func>
Result<Container, Key, Value> find_if(Container<Key,Value>& container, Func&& func)
{
    // we pass everything we have to std::find_if
    auto it = std::find_if(container.begin(), container.end(), func);
    if (it == container.end()) {
        // and return find_result without a found value
        return Result<Container, Key, Value>();
    } else {
        // or with a pointer to the found value
        return Result<Container, Key, Value>(&(*it));
    }
}    

And this allows me to do the following:

std::unordered_map<std::size_t, std::size_t> m =
{
    {1, 11},
    {2, 22},
    {3, 33}
};   
// ... 
cwt::find_if(m, [](const auto& pair){
    return pair.second == 22;
}).and_then([](const auto* pair){
    // pair->first is key 2
    // pair->second is value 22
    // we also could remove const 
    // if we want to modify the element
}).or_else([](){
    // do something else ...
});

It's almost the same issue with a std::vector. If we use just std::find_if it's almost the same:

auto it = std::find_if(m.begin(), m.end(), [](const auto& pair){
    return pair.second == 22;
});
if (it != m.end()) {
    // it->first is key 2
    // it->second  is value 22
} else {
    // do something else ... 
}

So it seemed promising to me, but I haven't gotten anything out of it yet. And that would be a little bit of overkill that wouldn't bring a real result or a real improvement.

But there is another use case: Getting a value from a key in a map. Like .at() or the operator[].

For me, there were a few things to consider. I was working on a Lua API where the calls with the keys which I stored internally. And there were a few things to consider:

  • I don't want to throw an exception with .at().
  • I don't want to use try/catch on every call.
  • I don't want to add elements to the container with opeartor[]. An invalid key should just result in a error log.
  • I don't have C++23 available on the target plattform.

So basically I got rid of the find/find_if wrapper. I just used the find_result type and wrapped this into a get_value(..):

// same template signature here
template<template<typename...> typename Container, typename Key, typename Value>
// we return a find_result which holds a pointer to the dedicated value
details::find_result<Value*> get_value(Container<Key,Value>& container, const Key& key)
{
    // we check if the key is in our container
    if (container.count(key)) {
        // and return a reference in find_result with the value
        return details::find_result<Value*>(&container.at(key));
    } else {
        // else we have here the error event / notification 
        // for an invalid keys and return an empty find_result
        return details::find_result<Value*>();
    } 
}    

And this allowed me to write my code like this:

// any map for demonstration
std::unordered_map<std::size_t, std::size_t> m =
{
    {1, 11},
    {2, 22},
    {3, 33}
};
// ...

// we call get_value and execute .and_then only if we found 
// the value. if not we just ignore the lambda here
cwt::get_value(m, std::size_t{2})
    .and_then([](const auto* value){
        std::cout << "found value: " << *value << std::endl;
    });

And this was quite useful in my particular case. I have several accesses to the container where it's not guaranteed that the value is present. And to avoid always checking if the key exists and logging the error, I can now just use .and_then(..) and execute the lambda if the key exists.

Conclusion

You can find the last example here on compiler explorer.

So my final thoughts, the find wrappers were overkill and using them feels wrong. But for the access to a key in a map as in the last example with get_value it seems to make more sense.

I was able to isolate the error case and only execute my lambda with a key that was found. This improved readability. I only had to express the error case once. Once I have C++23 available I could just use std::optional and I can remove my find_result to have the same behavior. And since I named the functions the same, refactoring wouldn't be a big deal then.

It always serves as a little inspiration and expands thinking, even if it was not the result I expected. But that is how things sometimes are and it still was kind of fun to try to achieve something here.

I hope you enjoyed reading it. Until the next one.

Best Thomas

Previous
Previous

My Sphinx Best Practice Guide for Multi-version Documentation in Different Languages

Next
Next

[C++] A find-Function to Append .and_then(..).or_else(..) in C++