0

I have code like this:

std::map<int, const Data> all_data;
// ...
bool OldDataIsBetter(Data const& oldData, Data const& newData) {...}
// ...
void AddData(int key, Data&& newData)
{
    auto [it, ok] = all_data.try_emplace(key, std::move(newData));
    if (!ok)
    {
        if (OldDataIsBetter(*it, newData)) return;
        it->second = std::move(newData);
    }
}

This doesn't compile, because it->second refers to a const Data and so its assignment operator cannot be called. It works fine if const is removed.

The intent of the above is to insert_or_assign, except that if an item is already present then I need to compare the old and new items to see which one is "better".

The intent of declaring the map element type with const is that the data is supposed to be immutable once in the map -- the item as a whole can be replaced but not piecemeal modified.

I can "fix" the above by reassigning to the container instead:

all_data[key] = std::move(newData);

(Actually turns out that has the same problem with const.)

Or by erasing and retrying the emplace:

all_data.erase(it);
all_data.emplace(key, std::move(newData)); // should never fail

But neither of these seem elegant, since I already have an iterator pointing to the item that should be replaced, and both of the above forget that and go search again.

Is there a better way to accomplish this replacement?


TLDR from a chat thread brought up related questions:

  • If extract can remove a node from a container, let you modify its otherwise-const key, and reinsert it -- all without any reallocations -- why is this not possible for a const mapped_value?
  • Const objects can be destroyed. This should also apply to const objects inside containers -- and indeed that's what the erase/emplace variant might be doing if it happens to reuse the same storage for the node (either by coincidence or through a custom allocator). Why then isn't there a way to replace a const mapped_value with a different const mapped_value without reallocating the map node that contains it?
Miral
  • 12,637
  • 4
  • 53
  • 93
  • "*It works fine if `const` is removed.*" I guess that brings us to the obvious question: were you *serious* when you declared that the data was `const`? Because if you want to change a thing, then that means you weren't actually serious when you declared said thing to be `const`. Because `const` means you can't change it. You contradicted yourself, and C++ doesn't really like it when you do that. – Nicol Bolas Jan 14 '20 at 04:21
  • I don't want individual members to be modified. I don't mind the whole object being replaced. Unfortunately C++ `const` combines both of those concepts together. (Which does admit another option, using an non-const immutable wrapper (eg. `std::unique_ptr`, with the pointer itself non-const). But that adds an annoying indirection at access, so I consider it worse.) – Miral Jan 14 '20 at 04:32

3 Answers3

3

const means immutable, not partially mutable. If you use const on an object declaration (which is what you're doing when you stick const in that template parameter), C++ believes you mean it. And it will hold you to it.

So const Data is a non-starter.

From your question, I surmise that Data has some functions that set some parts of its state, and you do not want users to call said functions. But you want the user to be able to overwrite the value.

The way to do this is to provide an object type which wraps a Data instance, allowing assignment from a Data object. This wrapper will provide versions of the const accessors that Data provides. But the type does not otherwise provide non-const modifiers of Data.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • That's what I suggested in my comment, yes. But that also seems like a non-starter, due to the extra indirection cruft (either by explicitly having to `*` or `->` indirect or implement a wrapper that delegates all method calls to an internal non-const instance). "Replace which object this map node points to without modifying the original object" seems like an operation that ought to exist, though. – Miral Jan 14 '20 at 04:37
  • @Miral: "*That's what I suggested in my comment, yes.*" No, your comment suggesed a `unique_ptr`, which requires heap allocation. I'm talking about a wrapper that *contains` a `Data` instance, not points to one. "*Replace which object this map node points to without modifying the original object*" That's a contradiction; you cannot replace an object without modifying it. Not unless you destroy it, and then you get into `std::launder` territory. – Nicol Bolas Jan 14 '20 at 04:40
  • An object that has been const for its entire lifetime is still allowed to have its destructor be called. Why can't the map destruct the old object and construct a new one in its place without violating const-correctness? – Miral Jan 14 '20 at 04:41
  • @Miral: [That's why](https://stackoverflow.com/a/39382728/734069). – Nicol Bolas Jan 14 '20 at 04:42
  • I don't think that argument applies when we're talking about something inside a runtime container. The compiler can't make assumptions about what values it contains anyway. – Miral Jan 14 '20 at 04:43
  • @Miral: "*I don't think that argument applies when we're talking about something inside a runtime container.*" A runtime container which is *required by the standard* to contain `pair` values. If that `value` is `const`, then you need `launder` mechanics in order to destroy and recreate it. And by "you", I mean any code that attempts to access the `map::value_type`, such as any interface using iterators. Oh, and `operator[]` has to return a `value&`, so how could a `value&` provoke a destroy/reconstruct cycle anyway? – Nicol Bolas Jan 14 '20 at 04:44
  • Yes, but they're not on the stack, they're behind an iterator, which is a pointer to heap memory (and usually not directly to a `pair` but actually to a container node). Either the value within the pair or the whole pair could be destroyed and reconstructed. It's not even that different from what happens in my answer, except that it skips a memory reallocation. (Which a pool allocator might end up using the same storage for anyway.) – Miral Jan 14 '20 at 04:49
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/205905/discussion-between-nicol-bolas-and-miral). – Nicol Bolas Jan 14 '20 at 04:50
1

In your case, what you really want to be able to do is conditionally overwrite the data element.

This strongly argues for a mutable data element.

In the general case, the container must have immutable elements, so you could provide a wrapper that provides const access unless mutable access is specifically requested:

#include <map>
#include <functional>

struct Data {};

struct MyDataMap
{
    using store_type = std::map<int, Data>;
    using iterator = store_type::const_iterator;

    template<class Condition>
    std::pair<iterator, bool> 
    replace_if(int key, Data&& value, Condition cond)
    {
        auto [it, inserted_or_replaced] = store_.try_emplace(key, std::move(value));
        if (!inserted_or_replaced && cond(it->second, value))
        {
            it->second = std::move(value);
            inserted_or_replaced = true;
        }
        return std::make_pair(it, inserted_or_replaced);
    }

    // other accessors as necessary

    iterator begin() const { return store_.cbegin(); }
    iterator end() const { return store_.cend(); }

private:

    store_type store_;
};

// ...
bool OldDataIsBetter(Data const& oldData, Data const& newData);
// ...

void test(MyDataMap& m, int k, Data newd)
{
    auto oldIsWorse = std::not_fn(OldDataIsBetter);
    auto [it, replaced] = m.replace_if(k, std::move(newd), oldIsWorse);    
}
Richard Hodges
  • 68,278
  • 7
  • 90
  • 142
  • That's doable, but it requires replicating most of the `std::map` interface (and via delegation, not `using`, because the types are different). This does not seem like an improvement. – Miral Jan 14 '20 at 22:30
  • @Miral I don't have much information to go on about your use case. I would imagine it would cost about 30 minutes to add the necessary interfaces you need. In return for that you get improved runtime performance. Removing an re-adding an item to a maep can be little expensive. all the best. – Richard Hodges Jan 14 '20 at 22:45
  • I tried it with private inheritance of `map` instead; this allowed `using` 10 methods (`map`,`clear`,`insert_or_assign`,`emplace`,`erase`,`empty`,`size`,`cbegin`,`cend`, `swap`) and delegating 3 (`begin`,`end`,`find`, to hide their non-const versions), which wasn't too terrible. This still isn't the complete map interface, but it's sufficient for my needs. – Miral Jan 14 '20 at 23:17
  • Actually I had to delegate `erase`, `emplace`, and `insert_or_assign` as well, otherwise they could leak a non-const iterator in their return value. idk, this still seems like something that should already be in the library. – Miral Jan 14 '20 at 23:48
0

After some further experimentation I think that the below is the best method currently available for this scenario:

it = all_data.erase(it);
all_data.emplace_hint(it, key, std::move(newData)); // should never fail

This still doesn't seem ideal to me as it will still reallocate the map node. This is not the end of the world but I'd like to find a way to avoid it if possible.

Miral
  • 12,637
  • 4
  • 53
  • 93
  • The way to avoid it is to follow Nicol's advice and make the data item mutable in this map. This is semantically correct for your use case. Will provide an answer for illustration. – Richard Hodges Jan 14 '20 at 10:29