2

Today I saw my boss's code which uses a const reference as a map's value type.

Here's the code:

class ConfigManager{
    public:
        map<PB::Point, const PB::WorldPoint&, compare_point> world_point;
    //the rest are omitted
};

(PB is Google Protobuf, we are using the Protobuf library. I don't know much about it or if it's relevant to the question. )

What this class does is that it reads some config files and put it into some maps for searhing.


At first I was surprised because I haven't seen a map with a reference as its value, which is e.g. map<int, classA&> aMap.

So then I searched on SO and these 2 questions tell me that I can't do that.

C++: Is it possible to use a reference as the value in a map?

STL map containing references does not compile

Then I tried this code, indeed it doesn't compile:

Code Example1

struct A {
    int x = 3;
    int y = 4;
};

map<int, A&> myMap;

int main() {
    A a;
    myMap.insert(make_pair(1, a));
}

But if I change map<int, A&> myMap; to map<int, const A&> myMap;, it compiles.

Yet another problem occured. With map<int, const A&> myMap;, I can't use [] to get the pair, but I can use map.find().

(My boss told me to use map.find() after I told him using[] can't compile).

Code Example2

struct A {
    int x = 3;
    int y = 4;
};

map<int, const A&> myMap;

int main() {
    A a;
    myMap.insert(make_pair(1, a));

    //can't compile
    cout << myMap[1].x << " " << myMap[1].y << endl; 
    
    //can work
    //auto it = myMap.find(1);
    //cout << it->second.x << " " << it->second.y << endl;
}

So till here I was thinking my boss was correct. His code was correct.


The last story is that I showed the code to some online friends. And they noticed a problem.

Code Example3

#include <map>
#include <iostream>
#include <string>
using namespace std;

struct A {
    int x = 3;
    int y = 4;
    ~A(){
        cout << "~A():" << x << endl;
        x = 0;
        y = 0;
    }
};

map<string, const A&> myMap;

int main() {
    A a;
    cout << a.x << " " << a.y << endl;
    myMap.insert(make_pair("love", a));
    a.x = 999;
    cout << "hello" << endl;
    auto s = myMap.find("love");
    cout << s->second.x << " " << s->second.y << endl;
}

The output is:

3 4
~A():3
hello
0 0
~A():999

If I understand the output correctly(correct me if I get it wrong), it indicates that:

  1. make_pair("love", a) creates an object pair<"love", temproray copy of a>. And the pair gets inserted into myMap.
  2. Somehow, I don't know how it happens, the temporary copy of a gets destructed immediately. To me, it means the memory of the temporary copy of a is now not owned by anyone and it is now a free space of memory that can be filled with any values, if I understand correctly.

So now I am getting confused again.

My questions are:

  1. What happens to the Code Example3? Is my understanding correct? Why does temporary copy of a get destructed right after the statement? Isn't using a const reference can extend a temporary's lifetime? I mean, I think the it should not get destructed till main finishes.

  2. Is my boss's code incorrect and very dangerous?

Rick
  • 7,007
  • 2
  • 49
  • 79
  • Sorry if the question is too long. I've tried my best to make it short. – Rick Oct 22 '20 at 13:14
  • [This example](https://godbolt.org/z/jfn4z1) leads me to believe that your boss' code doesn't work as intended. The map's referenced object doesn't appear to be the same as the original. – François Andrieux Oct 22 '20 at 13:25
  • The bottom line should be that, even if it *did* work as intended, the fact that it results in this level of research shows that it should be avoided anyway. – François Andrieux Oct 22 '20 at 16:37

3 Answers3

2

Why does temporary copy of a get destructed right after the statement?

Because (in most cases) that's how temporaries work. The live until the end of the statement in which they are created. The extension to a temporaries lifetime doesn't apply in this case, see here. The TLDR version is

In general, the lifetime of a temporary cannot be further extended by "passing it on": a second reference, initialized from the reference to which the temporary was bound, does not affect its lifetime.

can I use const reference as a map's value type?

Yes as long as you realise that adding a const reference to a map has no effect on the lifetime of the object being referred to. Your bosses code is also incorrect because the temporary returned by make_pair is destroyed at the end of the statement.

john
  • 85,011
  • 4
  • 57
  • 81
  • @FrançoisAndrieux Yes, didn't look at the code long enough – john Oct 22 '20 at 13:22
  • This would benefit from mentioning `std::ref` – StoryTeller - Unslander Monica Oct 22 '20 at 13:30
  • @StoryTeller-UnslanderMonica What can `std::ref` do in here? – Rick Oct 22 '20 at 15:40
  • @Rick - It can be used to create a pair that holds a reference wrapper to `a`, instead of a copy. Thus initializing the item in the map with a reference to `a` (instead of dangling). – StoryTeller - Unslander Monica Oct 22 '20 at 15:41
  • Sorry I don't understand your answer. Which rule of https://en.cppreference.com/w/cpp/language/reference_initialization#Lifetime_of_a_temporary, applies in this case so that the temporary object's lifetime doesn't get extended? It would be much appreciated if your answer can be longer. – Rick Oct 22 '20 at 15:42
  • To me, `make_pair("love", a)` returns a `pair<"love", copy of a>` and `copy of a` is assigned to `const A&`, there's only 1 reference here. – Rick Oct 22 '20 at 15:45
  • @Rick It is hard to illustrate without going into speculation of `std::map` implementation details. But the in short the properties of references extending the life time of temporaries does not propagate. None of the cases that allow lifetime extension covers that case. – François Andrieux Oct 22 '20 at 16:02
  • @FrançoisAndrieux FYI, the link given by John is about the **exception cases**. *Whenever a reference is bound to a temporary or to a subobject thereof, the lifetime of the temporary is extended to match the lifetime of the reference, **with the following exceptions**: ......*, so which **exceptions** exactly covers this case? – Rick Oct 22 '20 at 16:05
  • @Rick Edit : Never mind, its this exception : "a temporary bound to a reference parameter in a function call exists until the end of the full expression containing that function call:" – François Andrieux Oct 22 '20 at 16:08
1

You may use std:: unique_ptr<A> instead. Then emplace instead of insert:

using value_t=std:: unique_ptr<A>;
std::map<int, value_t> myMap;
myMap.emplace(1,new A);
myMap[1]=new A{5,6};
myMap[1]->x=7;

more on std:: unique_ptr<A>: https://en.cppreference.com/w/cpp/memory/unique_ptr

Red.Wave
  • 2,790
  • 11
  • 17
  • 1
    The goal seems to be to have a map that refers to elements that exist outside the map. I'm not sure that this answer is an equivalent alternative. – François Andrieux Oct 22 '20 at 14:06
  • 1
    Since C++14 there is `std::make_unique` to replace `new T` when initializing `std::unique_ptr`s. In typical modern C++ the only use case for `new` is now placement. – François Andrieux Oct 22 '20 at 14:07
  • 1
    In c++20 there are enhancements to deductions for operator new. If the goal was reference ti existing objects, temporaries would not be used. Raw pointer is best for observer-only solutions. – Red.Wave Oct 22 '20 at 14:12
  • Well, thank you for helping. But I am not looking for an alternative. I more want to know why. – Rick Oct 22 '20 at 16:18
1

What happens to the Code Example3? Is my understanding correct?

Your explanation is close. The std::pair that is returned by std::make_pair is the temporary object. The temporary std::pair contains the copy of a. At the end of the expression the pair is destroyed, which also destroys its elements including the copy of a.

Why does temporary copy of a get destructed right after the statement? Isn't using a const reference can extend a temporary's lifetime? I mean, I think the it should not get destructed till main finishes.

The temporary here is the result of std::make_pair which is being used as an argument to the member function insert. The relevant rules that apply here are :

Whenever a reference is bound to a temporary or to a subobject thereof, the lifetime of the temporary is extended to match the lifetime of the reference, with the following exceptions:

  • [...]
  • a temporary bound to a reference parameter in a function call exists until the end of the full expression containing that function call [...]
  • [...]

source

The full expression containing the function call is the expression myMap.insert(make_pair(1, a)); This means that the lifetime of the result of std::make_pair ends after the function return, including the A it contains. The new std::map element will refer to the A in the temporary std::pair which will become dangling once insert returns.

Is my boss's code incorrect and very dangerous?

Yes, myMap contains a dangling references.

Rick
  • 7,007
  • 2
  • 49
  • 79
François Andrieux
  • 28,148
  • 6
  • 56
  • 87
  • I would like to point out that it's `std::pair`, and `V` happens to be a reference type, but `value_type` of the `map` itself, is not one. Anyway, I get the idea. – Rick Oct 22 '20 at 23:46
  • @Rick The pair I refer to when I say `std::pair` in my answer is the result of `std::make_pair`, `std::pair` or `std::pair` (depending on the example). You may be looking at `std::map::value_type` which is indeed `std::pair` (or `std::pair`, depending again) but that has no bearing on `std::make_pair` which only looks at its arguments. – François Andrieux Oct 23 '20 at 00:43
  • But you have to specifiy the `map` is like `std::map::value_type` form. Otherwise it doesn't match my case. Corresponding to the excpetion rule we have agreed, it's *a temporary bound ( `A` of `std::pair` returned by `std::make_pair`) to a reference parameter (`const A&` of `std::map::value_type`) in a function call (`std::map.insert()`) exists until the end of the full expression (`myMap.insert(make_pair("love", a));`) containing that function call* – Rick Oct 23 '20 at 00:59
  • Anyway, I think we both get the idea. I just think your answer need some modification to better match my case. :P – Rick Oct 23 '20 at 01:04
  • @Rick Its not clear to me which part exactly would need improvement. I would appreciate it if you could indicate it up for me. – François Andrieux Oct 23 '20 at 01:08
  • Check this https://chat.stackoverflow.com/transcript/message/50753330#50753330 maybe? I don't think your answer has something wrong. – Rick Oct 23 '20 at 01:21