2

My implementation uses std::map to store data. When I started my code it seemed like the best option. Now I came to a point where I have to change the key values of all objects inside the map.

The problem is that each object points to another object inside the map:

class AND : public node{
    vector <node*> inputs;
    vector <node*> outputs;
}

And the map is declared like this:

map<unsigned int, AND> all_ANDs;

My question is: If I use map::extract from C++17 to modify the key values in all_ANDs map, will my pointers (E.g. the ones inside the attribute inputs) keep pointing to the right places?

In other words: If I change the value of "first" element with extract, the address of "second" will keep intact?

I noticed from this link that the string "papaya" stays the same (and works gracefully). But I wanted to be sure about pointers.

gudé
  • 89
  • 1
  • 12
  • 2
    This is quite obviously a [XY problem](https://en.wikipedia.org/wiki/XY_problem). The fact that you need to change the keys for a map means that your design is flawed. Change the design. Also: what have you tried so far to answer your question? Why not simply try to `extract` an element and re-`insert` it with another key, then check for its address? If we can do that, then so can you! – Walter Nov 07 '18 at 19:22
  • One way to make sure, is to make your object `AND` non-movable and non-copyable. Then the compiler will puke if the address must be changed. – Walter Nov 07 '18 at 19:29
  • @Walter "Why not simply try to extract an element and re-insert it with another key, then check for its address?" this is a terrible advise, you should not deduce requirements from behavior of particular implementation. – Slava Nov 07 '18 at 19:29
  • @Slave It's not a terrible but pragmatical advise. What you have done is to *assume* that the result of such an exercise can be used to *deduce* requirements. I never assumed that. The converse, though is true: if the experiment shows that the address changes in some application, then it doesn't matter whether the standard says otherwise. – Walter Nov 07 '18 at 19:31
  • @Walter but if address does not change the result is useless, thats why your advise is bad, you should rely on documented behavior – Slava Nov 07 '18 at 19:34
  • @Slava - since it is trivial to prove the negative, it's not unreasonable to ask why OP didn't. Obviously failing to prove the negative doesn't guarantee anything, but it's still an easy check that would demonstrate some effort. – Useless Nov 07 '18 at 19:39
  • @Useless I still do not think so - negative result could be a bug in particular implementation. – Slava Nov 07 '18 at 19:41
  • Then you should never test anything? I mean, I'd expect to check both because I still can't use my legal solution until the compiler/library bug is fixed. Anyway, as I said, it _shows research effort_. It's essentially free good will, whether you're saying "I think this ought to work but doesn't" or "this worked for me but I'm not sure it's portable". – Useless Nov 07 '18 at 19:50

3 Answers3

6

YES The reference you have already quoted in your posts clearly states that no elements are copied or moved. (This assumes that node in your code snippet does not refer to map::node_type).

The same holds for the insert operation of the map-node (after modifying its key):

If the insertion is successful, pointers and references to the element obtained while it is held in the node handle are invalidated, and pointers and references obtained to that element before it was extracted become valid. (since C++17)

However, accessing the object between extract()ion and re-insert()ion has undefined behaviour and its address taken whilst in the extracted state is of limited use. Quoting from the standard:

The extract members invalidate only iterators to the removed element; pointers and references to the removed element remain valid. However, accessing the element through such pointers and references while the element is owned by a node_­type is undefined behavior. References and pointers to an element obtained while it is owned by a node_­type are invalidated if the element is successfully inserted.

Explanation

Essentially, a map<> is implemented as a tree of nodes, each holding a key and T (which are exposed as pair<const Key, T> to the user). Nodes are allocated (typically) on the heap and the address of your object is related to that of a node. map::extract() un-links a node from its tree and returns a node handle (an object holding a pointer to a map node), but AFAIK the node itself is not re-allocated, moved, or copied. Upon map::insert(handle), the node is re-linked into the tree according to its (new) key. Again, this involves no re-allocation, move, or copy of the node.

Remark

The above is a rough sketch. How things are actually done is likely more complex and also implementation defined. As explained here a node_handle does allow to alter the key through the member function

Key &node_handle::key() const;

How this is done under the hood is not specified and I speculate that the implementation uses a union or some cast to allow this. Of course, the map has to present to users a pair<const Key,T> in order to prevent them from changing the key and hence breaking the map, but this is not of any concern for an element extracted from the map.

Walter
  • 44,150
  • 20
  • 113
  • 196
  • @Slava `insert` manual says that *"pointers and references obtained to that element before it was extracted become valid"* – HolyBlackCat Nov 07 '18 at 19:37
  • 1
    "_If the insertion is successful, pointers and references to the element obtained while it is held in the node handle are invalidated, and pointers and references obtained to that element before it was extracted become valid_" seems clear enough to me. (Edit - beaten by 13 seconds!) – Useless Nov 07 '18 at 19:37
  • I missed that, but that needs to be part of the answer, `extract` guarantee is clearly not enough in this case. – Slava Nov 07 '18 at 19:38
  • No, `node` is not `map::node_type`. Its a user defined class. – gudé Nov 07 '18 at 19:41
  • "holding a pair" now interesting question, how OP is going to change the key – Slava Nov 07 '18 at 19:51
  • 2
    The [node handle](https://en.cppreference.com/w/cpp/container/node_handle) exposes a _non-const_ key reference, just so you can mutate it while it's not in a map – Useless Nov 07 '18 at 19:52
  • This makes it sound as though implementing a map-like class with `extract` functionality isn't possible if you aren't the compiler vendor due to UB. Yet Boost has one: https://www.boost.org/doc/libs/1_65_1/boost/container/map.hpp Any idea what it's doing to make that work? – Ben Jun 09 '21 at 19:43
2

My above answer addresses your immediate question. However, as I have suggested in a comment, this appears to be a XY problem. What I suspect:

  1. You have some structure of AND objects which are interlinked via their inputs and outputs fields. This linkage must not be broken by any re-allocation, so you cannot store them in a growing vector<AND> with re-allocation.

  2. You also want to order these objects according to some key and have therefore stored them in a map<Key,AND>, which indeed does not re-allocate when grown.

  3. You now want to order them according to another key (and/or change all the keys).

(If you're actually are not interested in ordering but merely in finding your objects by their key, you should have used unordered_map instead of map, which supports find() in O(n) rather than O(log(n)) operations.)

I suggest a different layout of your data:

  1. You store your AND objects in a way that allows growing their number without re-allocation. An obvious choice here is deque<AND>, since

    insertion and deletion at either end of a deque never invalidates pointers or references to the rest of the elements

    You may also make AND non-copyable and non-movable, ensuring that once allocated their address never changes (and pointers to them remain valid until destruction).

  2. You can support any find-by-key or order-by-key operations by actually working on pointers to the stored objects, either by sorting a vector of pair<key,AND*> or by using a map<key,AND*> or unordered_map<key,AND*>. You can even simultaneously have various keys per object (and a map for each).

  3. When you must re-key all objects, simply forget the old map and make a new one: since the map only stores pointers and not the objects, this does not affect your linkages.

Walter
  • 44,150
  • 20
  • 113
  • 196
  • Yes Walter you are totally right. The issue considering my implementation is like you described. Although I need them ordered. The map's find I use on another branch of my project that does other stuff. I was considering changing to vector and put the key values as AND's attribute and using sort based on this attribute. – gudé Nov 08 '18 at 19:52
  • Basically, I have the class GRAPH which holds the ANDs objects on a map. This class graph I use to read some files, structure these things based on the files and then do some computation on the structured data. Now I came to a point where I am creating my own files with the class SYNTHESIZER, which inherits from GRAPH and the map structure has no use in this case. I am thinking about removing the inheritance and changing from map to vector in the synthesizer class, as you suggested with deque, although I will need the ANDs sorted to write the file. – gudé Nov 08 '18 at 19:57
0

Your map holds actual AND objects, not pointers to objects. So, if the AND* pointers stored inside your vectors are pointing at the map's AND objects, then those pointers WILL become invalid once those objects are erased from the map.

However, extraction merely unlinks a specified node from the map, the node and thus its key and value are still valid in memory. The node can be re-inserted into a map without affecting the addresses of the node's key and value. In this regard, the pointers in the vectors WILL NOT become invalid (although it is undefined to dereference them while the node is detached from the container).

Another option is to change your map to hold AND* pointers instead. Or better, consider using std::shared_ptr<AND> in the map and std::shared_ptr<node> in the vectors, instead of raw pointers. Then it won't matter whether the map entries are erased or extracted, the AND objects will remain valid as long as there are active shared_ptr references to them.

Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • "YES, those pointers will become invalid once those objects are removed from the map for any reason" this is not what `extarct` documentation says I believe – Slava Nov 07 '18 at 19:46
  • This is possibly a better design, but the extract/node handle mechanism added in C++17 does allow un-linking and re-linking tree nodes without change of address. – Useless Nov 07 '18 at 19:48
  • *those pointers will become invalid once those objects are removed from the map* This is **wrong** for removal via `map::extract` – Walter Nov 07 '18 at 19:50
  • @Walter Actually it isn't, at least until you do a `map::insert` again – Lightness Races in Orbit Nov 08 '18 at 12:50
  • @LightnessRacesinOrbit How do you know? This makes no sense: the address of the object itself is re-stored, but was changed in between??? NO, the object was never moved/copied, only the handle was. – Walter Nov 08 '18 at 12:52
  • @Walter: I know because that's what the standard says. I can't tell you why it's that way, but the original pointers are invalidated for the duration of time that the node is extracted; they are re-validated once it's been re-inserted. You can read about this in [the reference](https://en.cppreference.com/w/cpp/container/map/extract). – Lightness Races in Orbit Nov 08 '18 at 12:53
  • @LightnessRacesinOrbit where does it say so? – Walter Nov 08 '18 at 12:53
  • @LightnessRacesinOrbit that's not the standard :-( – Walter Nov 08 '18 at 12:55
  • @Walter: http://eel.is/c++draft/associative.reqmts#10 (the wording actually is not quite what Remy said; the pointers are "valid" still but dereferencing them has UB until re-insertion of the node; so I guess it depends on your personal definition of "valid"!) – Lightness Races in Orbit Nov 08 '18 at 12:55
  • @LightnessRacesinOrbit The question really is whether `extract()` and `insert(node_type)` are supported for `mapped_type`s which cannot be moved or copied. I suspect they are for most implementations, but what does the standard say? – Walter Nov 08 '18 at 12:59
  • @Walter I'm not following you. You seemed to be saying that the pointers remain valid after `extract` and I've shown that this is not true (FSVO "valid"). Now you're asking something else? – Lightness Races in Orbit Nov 08 '18 at 13:00
  • @Walter: Requirements on `mapped_type` are found under http://eel.is/c++draft/associative.reqmts#7 and by extension http://eel.is/c++draft/container.requirements.general#tab:containers.allocatoraware – Lightness Races in Orbit Nov 08 '18 at 13:02
  • This only lists the requirements (on `key_type` and `mapped_type`) for the *copy-assign operation* of the container, but I'm interested in `extract()` and `insert(node_type)`. – Walter Nov 08 '18 at 13:09
  • @LightnessRacesinOrbit I don't understand the rationale between these rules (the UB when accessing the extracted object through its pointer and the fact that the address taken during the extracted state becomes invalid upon re-insertion). Do you? – Walter Nov 08 '18 at 13:26
  • @Walter _"I don't understand the rationale [...] Do you?"_ Not really, no. I had this conversation with someone a couple of weeks ago too and it was not enlightening. – Lightness Races in Orbit Nov 08 '18 at 15:11
  • @Walter As for requirements on the mapped_type as they apply to extract and insert, you've lost me: what requirements are you interested in, and how does that relate to the question? – Lightness Races in Orbit Nov 08 '18 at 15:11