I am trying to create a class that instantiates an std::pair<const T&, const U&>
from two member variables. I started this out of curiosity and was surprised to see the results from creating multiple objects of, in the example below, Pair
.
struct Pair {
int num;
string s;
pair<const int&, const string&> pr{make_pair(num, s)};
Pair(int n, const string& s) : num(n), s(s) {}
};
ostream& operator<<(ostream& out, const Pair& p) {
out << setw(2) << p.num << ", " << p.s << ": <" << p.pr.first << "," << p.pr.second << ">";
return out;
}
/******************************************************************************/
int main() {
cout << string(15, '-') << " PIECEMEAL " << string(15, '-') << endl;
Pair p1{1, "normal1"};
Pair p2{2, "normal2"};
cout << p1 << endl;
cout << p2 << endl << endl;
cout << string(15, '-') << " VECTOR " << string(15, '-') << endl;
vector<Pair> v;
for (int i=0; i< 05; i) {
v.emplace_back(i, "from_vec'" ::to_string(i) "'");
}
for(const auto & p : v) {
cout << p << endl;
}
}
I thought that the const
reference type declarations within the pair
would refer to the Pair
members, num
and s
, respectively. However, when I create multiple Pair
s, it appears that the references within the pair
end up referencing (to some degree) the Pair
members of the most recently constructed Pair
.
Here is the output:
--------------- PIECEMEAL ---------------
1, normal1: <2,normal2>
2, normal2: <1586964272,normal2>
--------------- VECTOR ---------------
0, from_vec'0': <4,from_vec'4'>
1, from_vec'1': <1586964272,from_vec'4'>
2, from_vec'2': <1586964272,from_vec'4'>
3, from_vec'3': <1586964272,from_vec'4'>
4, from_vec'4': <1586964272,from_vec'4'>
I looked at cppreference in an attempt to understand what was going on, but still not grasping it.
Why do the references within the pair
no longer reference the Pair
members, num
and s
?
CodePudding user response:
There are 2 problems with your code.
First:
pair<const int&, const string&> pr{make_pair(num, s)}
std::make_pair(num, s)
first create a temporary pair<int, string>
. Then pr
will be initialized with this temporary pair
.
Note that pair<int, string>
is not the same as pari<const int&, const string&>
, which means it will not trigger the default move constructor.
Instead, if you looked at cppreference, this would trigger the 6th overload, and essentially assign pr.first
and pr.second
with this temporary pair
, hence dangling reference.
To fix it, you should specifically create a pair
of references with:
pr{std::make_pair(std::ref(num), std::ref(s)}
Or just create pr
directly with num
and s
:
pr{num, s}
Second:
v.emplace_back(i, "from_vec'" ::to_string(i) "'");
This line would trigger the default move constructor, since you didn't specify one yourself, which would perform a member-wise move on Pair
.
However, what happens when you move pr
to the new Pair
? The old pr
was referencing the old num
and old s
. Moving the old pr
to the new Pair
doesn't change the value of pr
, hence it will continue referencing the old num
and old s
, hence dangling reference.
So instead of relying on default generated move constructors, you must define them manually:
Pair(Pair&& pair) noexcept
: num(std::exchange(pair.num, {})
, s(std::move(pair.s))
{}
Note, you don't need to construct pr
within initializer list since you already have a default initializer for pr
.
CodePudding user response:
You are inadvertently creating a pair that holds const references to another pair that was allocated on the stack and immediately freed. In other words, undefined behavior.
Step by step, here's what's happening when you instantiate a Pair:
- The Pair constructor is called.
- The Pair initializer is called.
- The initializer calls make_pair, creating a pair of int and std::string on the stack.
- This temporary pair gets assigned to pr, and the const references inside it now refer to the memory of the temporary pair.
- The initializer exits, freeing the temporary pair on the stack. the int and string references inside pr now refer to memory that has been freed.
What's more, you really shouldn't be storing references to other members inside your struct, for memory safety reasons. If you want a std::pair of your two values, you should make a member function that returns it instead of storing it inside the struct itself.