4

I'm trying to iterate through an object's member std::vectors, using a member std::tuple that contains the references of the member vectors.

A simple example would be like this:

#include <iostream>
#include <vector>
#include <tuple>
struct S {
    std::vector<int> a;
    std::vector<double> b;
    std::tuple<std::vector<int>&, std::vector<double>&> tup{a, b};
};
int main() {
    S s; 
    s.a = {2, 3};
    s.b = {5.1, 6.1};
    std::apply([&](auto&... v){
        ((std::cout << v.size()), ...);
    }, s.tup);
}

This works, but the problem is that if I assign the object s to an std container, the references tup is holding can be dangling references. Such as:

    std::map<int, S> myMap;
    myMap[3] = s; // s.a and s.b has been copied to different addresses.
    auto& v = std::get<0>(myMap.at(3).tup); // -> still refers to the previous s.a, not the copied one.

Is there any decent way to solve this problem? I want the references to refer to the newly copied members, not the original ones so that the member vectors of the new object can be iterated through using the member tuple.

(This question is being written after asking this question.)

starriet
  • 2,565
  • 22
  • 23
  • 4
    Do not have a data member `tup` that is initialized and copied around, but have a member function `tup()` that returns the tuple with the references. You can create it conveniently with `std::tie()`. – j6t Apr 14 '23 at 11:51
  • for future ref) Some points are: 1) use member function instead of member data, so that there is no data copy and thus no dangling reference. 2) it's convenient to return `std::tie`, which is equivalent to return the tuple `tuple&,...>tup{a,b,...}`. 3) see [this answer](https://stackoverflow.com/a/51971629/10027592) for a nice explanation between `std::tie` and `std::forward_as_tuple`. – starriet Apr 14 '23 at 16:00

1 Answers1

4

The key problem is that references cannot be re-seated. So when you copy/move an S, you cannot make the references inside the tuple point at the ones inside the new object.

The easiest way is to just make a member function and not have a data member:

struct S {
    std::vector<int> a;
    std::vector<double> b;

    auto tup() {
        return std::tie(a,b);
    }
};

If you really want to have it as member data, you can instead use raw pointers. Having non-owning raw pointers is totally fine. This way, you can write custom copy/move operations which fix up the pointers:

struct S {
    std::vector<int> a;
    std::vector<double> b;
    std::tuple<std::vector<int>*, std::vector<double>*> tup{&a, &b};

    S() = default;
    S(const S& other) : a(other.a), b(other.b), tup(&a, &b) {}
    S(S&& other) : a(std::move(other.a)), b(std::move(other.b)), tup(&a, &b) {}
    S& operator=(const S& other) { a = other.a; b = other.b; tup = std::make_tuple(&a, &b); }
    S& operator=(S&& other) { a = std::move(other.a); b = std::move(other.b); tup = std::make_tuple(&a, &b); }
};

If you really want them to be accessible as references for usability purposes, you could make tup a private member and write a member function which returns a tuple of references.

cigien
  • 57,834
  • 11
  • 73
  • 112
TartanLlama
  • 63,752
  • 13
  • 157
  • 193
  • 2
    Custom copy constructor doesn't need to re-seat. `S(const S& other) : a(other.a), b(other.b), tup(a,b){}` . Assignment operator doesn't need to re-seat either. After assigning, `a` and `b` are still the same members. (That said, the member function is still a better idea.) The root cause is that the default copy constructor will copy the references from the source, and we don't want that. – Raymond Chen Apr 14 '23 at 12:33