1

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.

user877329
  • 6,717
  • 8
  • 46
  • 88
  • 1
    The `[]` operator certainly returns a reference, and iterator-based access does not move or copy anything unless you write the code to do so. So what exactly are you asking? The `[]` operator will default-construct the instance of the value class, and the subsequent assignment will move-assign it. So, as long as your class has a default constructor and a move assignment operator, you're all set. – Sam Varshavchik Oct 27 '18 at 15:24
  • The map must not be updated. It should be const method that returns whats already in the map if it exists, otherwise it should return a specified default value. – user877329 Oct 27 '18 at 15:39
  • There's no such `std::map` method. You must implement this functionality yourself, by using `find()`, and then working with the found value, or using a default one. There are very few magic buttons in C++ that do what you want simply by pushing. There are a few of them, but if none of them do what you need to do, you have to invent and build this magic button all by yourself, in C++. I think what you want to do is use a helper class that will own a default instance of the value, or return a reference to the one in the map, and have your custom lookup code instantiate it, accordingly. – Sam Varshavchik Oct 27 '18 at 15:41
  • What's wrong with the answer given in the question you linked? – super Oct 27 '18 at 15:50
  • @super It fails for move-only types. Since the map itself has to be const, you cannot move from it, so a copy is required when fetching from the map as it is not possible to return by `const T&` when using the temporary as source (it will have been destructed). – user877329 Oct 27 '18 at 16:08
  • 1
    @user877329 Why do you need to use a temporary as source? You could for example have a `static T default_value;` in your function and return a reference to that. – super Oct 27 '18 at 16:30
  • @super Good workaround! But it requires proper documentation to prevent bugs, which not that good. – user877329 Oct 27 '18 at 17:09

1 Answers1

1

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.

Michael Veksler
  • 8,217
  • 1
  • 20
  • 33