0

The following code shows a very basic but essential functionality I would like to create and am getting some nasty run-time errors currently which I have not been able to debug myself. I have been working to write a solution to what I am looking for and this is the closest I have come. Any help in identifying a fix or re-design of this implementation is greatly appreciated!

This is the class. What I am looking for is a map which can still perform its operator[key] functionality but also be iterated through in sequential order as the elements were added. I am attempting to do this by having a map for lookup with its value being a pointer to the real value being held in a vector of corresponding pairs.

template <typename t>
class IndexedMap {
 public:
  t& operator [] (string s) {
    bool nu = true;

    for (auto& e : actual) // for each pair
      if  (e.first == s) // if exist
    nu = false; 
    if (nu == true) { // if needs created
      actual.push_back(pair <string, t>()); // create proper pair
      actual.back().first = s; // assign key
      // create copy in map @ same key pointing to actual value
      lookup[s] = &actual.back().second;
    }
    return *lookup[s]; // return reference to value
  }

  typename vector <pair <string, t>>::iterator begin () {
    return actual.begin();
  }

  typename vector <pair <string, t>>::iterator end () {
    return actual.end();
  }

 private:
  vector <pair <string, t>> actual;
  map <string, t*> lookup;
};

This implementation "works" with the following test.cpp- meaning that it will run and I actually do see the result I am looking for, but upon exit of test.cpp I am getting some crazy errors involving a call to free() and I am not sure how that is taking place or how to fix.

test.cpp :

int main () {
  IndexedMap <vector <int>> test;

  test["BILLS"]; test["GAS"]; 
  test["GROCERY"]; test["PETS"]; 
  test["TAKEOUT"]; test["OTHER"];

  int i = 0;
  for (auto e : test) // check order
    cout << e.first << endl;
  for (auto& e : test) // assign 3 unique values to each vector
    for (int f = 0; f < 3; ++f, ++i)
      e.second.push_back(i);
  for (auto e : test) { // display them
    cout << e.first << ":" << endl;
    for (auto f : e.second)
      cout << f << endl;
  }
  vector <int> blank; // test modifying a value
  test["GAS"] = blank;
  for (auto e : test["GAS"])
    cout << e << endl;
  cout << "hopefully empty?" << endl;
}

I hope this is not all too confusing the way I have explained or written this out. Many thanks in advance to any help that can be provided.

Happy new year everyone!

  • There's too much wrong with the code for a single SO question. You should focus your question on a single, clear issue, and post a relevant [mcve]. Also, it was already explained in comments to an earlier question why storing pointers to elements of a vector won't work. – juanchopanza Dec 31 '17 at 08:00
  • @juanchopanza I looked back and actually could not find a clear enough indication of WHY the pointers wont work, and that is the primary reason I am searching for the concise explanation to this issue. I thought my question was well limited in its scope ... – JosephJerrel Dec 31 '17 at 08:03
  • 1
    So this comment contains crucial info: "Better map into the vector's indices, because pointers will easily get invalidated". Then what you do is research vector pointer or iterator invalidation. – juanchopanza Dec 31 '17 at 08:06
  • @JosephJerrel Regarding the _pointers_: https://stackoverflow.com/questions/46991224/are-there-any-valid-use-cases-to-use-new-and-delete-raw-pointers-or-c-style-arr – user0042 Dec 31 '17 at 08:08
  • @juanchopanza I follow that vauge explanation just fine .. I just dont understand where specifically invalidation is occurring. I am not destroying an element of either container at any point. Dont assume I havent given much effort to deduce this answer alone already. Just looking for some help. If you dont want to give any then dont comment. Thats more helpful than you trying to make me feel bad about not already finding the answer by alone man. Ive spent all day lol. – JosephJerrel Dec 31 '17 at 08:11
  • Here's a relevant link: https://stackoverflow.com/questions/6438086/iterator-invalidation-rules – juanchopanza Dec 31 '17 at 08:17
  • @juanchopanza I do appreciate you help very much my brother and I will take a detailed look at those links. The second one I am familar with but will take a second look to see if anything is helpful. Could you possibly just provide me a simple suggestion towards a fix for this issue though lol? is there a safe checking operation I should perform or possibly a different pointer type? Or if this is just not possible to do could you give your opinion on that? lol .. because the alternative of returning only a value (copy of the value) is not sufficient for what I need to do. – JosephJerrel Dec 31 '17 at 08:25
  • Just a couple of hints: try using indices as in the answer to your previous question. And for look-up, use `std::map`'s functionality instead of rolling out your own. `std::map`. – juanchopanza Dec 31 '17 at 08:30
  • 1
    @juanchopanza WOW Ive actually got a working solution! Its taken me all day and now its thanks to your cryptic advice and incomplete suggestions for help that Ive arrived at working implementation. LOL , so thank you sir. You baited me in the right direction and I figured it out, so no sarcasm- genuinely I do appreciate it. – JosephJerrel Dec 31 '17 at 09:03

2 Answers2

0

Thanks to help from @juanchopanza Ive come up with a working solution for this problem.

Still unsure of exactly where or how the pointers are being invalidated in the previous implementation posted above, but now by using indices to identify the proper element in the vector and then returning that element itself, rather than a pointer to this location I am safe ;)

t& operator [] (string s) {
    bool nu = true;

    for (auto& e : actual) // for each pair
      if  (e.first == s) // if exist
        nu = false; 
    if (nu == true) { // if needs created
      actual.push_back(pair <string, t>()); // create proper pair
      actual.back().first = s; // assign key
      lookup[s] = actual.size()-1; // assign proper index in map @ same key 
    }
    return actual[lookup[s]].second; // return reference to value
  }
  • See my answer as to why. – Surt Dec 31 '17 at 09:19
  • A vector requires consecutive allocation of its elements, so when it grows, it may have to reallocate its internal array *at a different address*. When it happens, all pointers to vector elements become *dangling*. – Serge Ballesta Dec 31 '17 at 09:19
  • @SergeBallesta I did consider this but because my vector was only growing to size 6 and this was all done before any calls using the pointers were made I didnt think that re-allocation was the cause of the invalidation. – JosephJerrel Dec 31 '17 at 09:30
  • Next step: don't do your own (inefficient look-up) but use the map you already have. – juanchopanza Dec 31 '17 at 19:18
0

This line exposes the error

actual.push_back(pair <string, t>()); // create proper pair

and this is where the misery stems from.

lookup[s] = &actual.back().second; // a pointer to an element in a vector.

What happens when the vector resizes? a new array is allocated for the vector and the data from the old array is copied into the new array and the old array is deallocated. This invalidates the pointers into the old array.

Lets evaluate your solution a bit, you iterate through the vector to see if s exists, this is O(N) and if found you make a lookup in the map anyway of O(log N).

And we want to use an index into actual instead of a pointer, so your map pair should be

using mappair = std::pair<std::string,  int>;

So if we rewrite (untested code)

for (auto& e : actual) // for each pair
  if  (e.first == s) // if exist
    nu = false; 
if (nu == true) { // if needs created
  actual.push_back(pair <string, t>()); // create proper pair
  actual.back().first = s; // assign key
  // create copy in map @ same key pointing to actual value
  lookup[s] = &actual.back().second;
}
return *lookup[s]; // return reference to value

to

auto found = lookup.insert(mappair(s, -1));
if (found.second) { // true if new element
  actual.emplace_back(mappair(s,t());
  found->first.second = actual.size()-1;
}
return *found.first;
Surt
  • 15,501
  • 3
  • 23
  • 39
  • Many thanks for this help! In regards to the dangling pointers - I did consider this but because my vector was only growing to size 6 and this was all done before any calls using the pointers were made I didnt think that re-allocation was the cause of the invalidation. I assumed this would be the case only if the container continued to grow. – JosephJerrel Dec 31 '17 at 09:35
  • 1
    @JosephJerrel, during the addition of the 6 elements it might have grown 6 times depending on the implementation, typically it grows with a factor sqrt(2), so 1,2,3,5, 7 in your case. – Surt Dec 31 '17 at 09:41