Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Referencing objects in vector (Modern C++)

I'm sorry if this is very simple but I haven't played with C++ for more than 15 years. Consider this simple example: A vector contains objects of type A. An object of class B must reference an A object that resides in the vector. (Edit for clarification - class B must have a member that references the A instance)

Back in the day you would just declare an A* and be done with it, but how would one do it today with smart pointers? I don't want to store shared or unique pointers in the vector, because I don't want the A objects allocated all over the heap. They have to be in the vector itself.

like image 272
Jesper Avatar asked Jan 02 '23 13:01

Jesper


1 Answers

You have a few options, based on your requirements.

Non-owning raw-pointer A*

There is nothing wrong with having a non-owning raw-pointer in modern C++. If B needs a nullable reference to A and the A can be guaranteed to outlive B then a raw-pointer is perfectly fine. One reason that the reference might need to be nullable is if you need to default construct B and then set the reference later:

class B {
  A* a_ = nullptr;
public:
  void setA(A& a) { a_ = &a; }
};

int main() {
  std::vector<A> aVec(3);
  B b;
  b.setA(aVec[1]);
}

Reference A&

If the reference does not need to be nullable. If the reference is set in the constructor of B and never changes then you can use a reference A&:

class B {
  A& a_;
public:
  B(A& a) : a_(a) {}
};

int main() {
  std::vector<A> aVec (3);
  B b(aVec[1]);
}

std::reference_wrapper<A>

One problem with using a reference is that you can't reseat the reference to point to a different a which means you can't have a setA member function and you can't have an assignment operator B::operator=(const B&). You could just use a non-owning raw-pointer and enforce that the raw-pointer should never be null. But the standard library now provides a convenience called std::reference_wrapper which can not be null like a reference but can be reseated like a pointer:

class B {
  std::reference_wrapper<A> a_;
public:
  B(A& a) : a_(a) {}
  void setA(A& a) { a_ = a; }
};

int main() {
  std::vector<A> aVec (3);

  B b(aVec[1]);
  B otherB(aVec[2]);

  b = otherB;  // use auto-generated assignment operator
  b.setA(aVec[0]);
}

index to an element in the vector

One common case is where the vector of A is growing and so it might re-allocate and invalidate references, pointers and iterators. In this case you could store an index to an element in the vector. This index will not be invalidated when the vector grows and also you can check the index is within bounds of the vector and not dangling:

class B {
  std::vector<A>& aVec_;
  int             aIndex_;
public:
  B(std::vector<A>& aVec, int aIndex) : aVec_(aVec), aIndex_(aIndex) {}

  void useA() {
    if (aIndex_ >= 0 && aIndex_ < aVec_.size()) {
      auto& a = aVec_[aIndex_];
      // use a ...
    }
  }
};

int main() {
  std::vector<A> aVec (3);

  B b(aVec, 1);
  b.useA();
}

If you are adding and removing from your vector of A then none of these approaches will work and you might need to reconsider your design.

like image 164
Chris Drew Avatar answered Jan 11 '23 16:01

Chris Drew