Search code examples
c++default-valuestdmap

std::map default value (move only types)


How to solve std::map default value for move-only types? It seems like the problem is who owns the object

  • If the value exists, the map stays owner, and a T const& must be returned
  • If the value does not exits, the caller will be the owner, and a T (move-constructed from the default value) must be returned.

But the return-type of the function must be the same regardless of where the return value came from. Thus it is impossible to take the default value from a temporary. Am I correct?

You could use std::shared_ptr but that would be cheating.


Solution

  • This can be achieved with a proxy object.

    template <typename T>
    class PossiblyOwner
    {
    public:
        struct Reference {};
    
        PossiblyOwner(const PossiblyOwner & other)
           : m_own(other.m_own), 
             m_ref(m_own.has_value() ? m_own.value() : other.m_ref)
        {}
        PossiblyOwner(PossiblyOwner && other)
           : m_own(std::move(other.m_own)), 
             m_ref(m_own.has_value() ? m_own.value() : other.m_ref)
        {}
    
        PossiblyOwner(T && val) : m_own(std::move(val)), m_ref(m_own.value()) {}
        PossiblyOwner(const T & val) : m_own(val), m_ref(m_own.value()) {}
        PossiblyOwner(Reference, const T & val) : m_ref(val) {}
        const T& value () const { return m_ref; }
        operator const T& () const { return m_ref; }
    
        // convenience operators, possibly also define ->, +, etc. 
        // but they are not strictly needed
        auto operator *() const { return *m_ref; }
    private:
        std::optional<T> m_own;
        const T & m_ref;
    };
    
    // Not strictly required
    template <typename T>
    std::ostream & operator<<(std::ostream & out,
                  const PossiblyOwner<T> & value)
    {
        return out << value.value();
    }
    template <typename Container, typename Key, typename ...DefaultArgs>
    auto GetOrDefault(const Container & container, const Key & key,
                      DefaultArgs ...defaultArgs)
        -> PossiblyOwner<decltype(container.find(key)->second)>
    {
        auto it = container.find(key);
        using value_type = decltype(it->second);
        using ret_type = PossiblyOwner<value_type>;
        if (it == container.end())
            return {value_type(std::forward<DefaultArgs>(defaultArgs)...)};
        else
            return {typename ret_type::Reference{}, it->second};
    }
    

    Then the usage can be:

    int main()
    {
        std::map<int, std::unique_ptr<std::string>> mapping;
        mapping.emplace(1, std::make_unique<std::string>("one"));
        mapping.emplace(2, std::make_unique<std::string>("two"));
        mapping.emplace(3, std::make_unique<std::string>("three"));
        std::cout << *GetOrDefault(mapping, 0,
                     std::make_unique<std::string>("zero")) << "\n";
        std::cout << *GetOrDefault(mapping, 1,
                     std::make_unique<std::string>("one1")) << "\n";
        std::cout << *GetOrDefault(mapping, 3,
                     new std::string("three1")) << "\n";
    }
    

    Edit

    I have noticed that the default copy constructor of PossiblyOwner<T> causes undefined behavior, so I had to define a non-default copy and move constructors.