简体   繁体   中英

std::unique_ptr with std::map

I have a std::map where the key is std::shared_ptr<Foo> and the value is std::unique_ptr<Bar> where Foo and Bar are very different classes from a third-party library. I am using this std::map object as an in-memory cache.

I am wondering what the best way of inserting a new entry into this map will be and then returned from a method, given that the Bar passed into the std::unique_ptr will already be constructed?

I currently have the following:

class SomeClass
{
public:

    const Bar* TryGetBarValue(std::shared_ptr<Foo> foo)
    {
        auto it = _cache.find(foo);

        if(it == _cache.end())
        {
            Bar bar = ThirdPartLibrary::CreateBar();
            _cache.emplace(foo, std::make_unique<Bar>(bar));
            return _cache.rbegin()->second.get();
        }

        //return result as raw ptr from unique_ptr
        return it->second.get();
    }

private:
    std::map<std::shared_ptr<Foo>, std::unique_ptr<Bar>> _cache;
}

EDIT

Thanks to the answer provided by Quentin, this is now my implementation:

class SomeClass
{
public:

    const Bar* TryGetBarValue(std::shared_ptr<Foo> foo)
    {
        auto it = _cachedImages.find(texture);

        if (it != _cachedImages.end())
        {
            return it->second.get();
        }

        return _cachedImages.emplace(
                std::move(texture), 
                std::make_unique<sf::Image>(texture->copyToImage())
            ).first->second.get(); 
        }

private:
    std::map<std::shared_ptr<Foo>, std::unique_ptr<Bar>> _cache;
}

Thanks for all your help!

return _cache.rbegin()->second.get(); does not do what you want, as std::map does not append elements but sorts them. However emplace returns an iterator to what it just inserted, so you only need:

return _cache.emplace(foo, std::make_unique<Bar>(bar))->first->second.get();

Or even, since you don't actually need to store and copy the Bar , and you can also sacrifice foo :

return _cache.emplace(
    std::move(foo),
    std::make_unique<Bar>(ThirdPartLibrary::CreateBar())
)->first->second.get();

I'd also personally flip the (it == _cache.end()) condition to make it an early return, but that's just a matter of taste.

Otherwise, what you have looks good to me.

You tagged this as c++14, but for posterity I'll add a C++17 version:

const Bar* TryGetBarValue(std::shared_ptr<Foo> foo)
{
    struct DelayedBar
    {
        operator std::unique_ptr<Bar>() const { return std::make_unique<Bar>(thirdpartyLibrary::CreateBar()); }
    };
    return _cache.try_emplace(std::move(foo), DelayedBar()).first->second.get();
}

The try_emplace function will emplace its arguments if the map doesn't already contain that key. If the key already exists, no object is constructed. In either case an iterator to that key/value pair is returned. This function avoids the double lookup involved when you do find -> emplace/insert .

In our case we can't simply pass the arguments of try_emplace along so I've tried to be clever in delaying the construction of the object using this DelayedBar class. It only calls CreateNewBar when trying to cast to a std::unique_ptr<Bar> which only happens when try_emplace is trying to construct the object.

I have compiled this with GCC 8.2, Clang 7.0.0 and MSVC 19.16 (all via Compiler Explorer) and it compiles okay.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM