Home > front end >  Understanding std::pair <const T&, const U&>
Understanding std::pair <const T&, const U&>

Time:01-19

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 Pairs, 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.


Demo

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:

  1. The Pair constructor is called.
  2. The Pair initializer is called.
  3. The initializer calls make_pair, creating a pair of int and std::string on the stack.
  4. This temporary pair gets assigned to pr, and the const references inside it now refer to the memory of the temporary pair.
  5. 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.

  • Related