Search code examples
c++std-pair

Understanding std::pair <const T&, const U&>


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?


Solution

  • 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