Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Standard Practice for Creating a "Vector of References" Using Only the Standard Libraries

I would like to create an object, put the object into a vector, and still be able to modify the same object by accessing only the vector. However, I understand that when an object is push_back() to a vector, the object is actually copied into the vector. As a result, accessing the object in the vector will merely access a similar, but different object.

I have a beginner's knowledge in C, so I know that I can create a pointer to the object, and make a vector of pointers. e.g. vector<Object *>. However, it seems as if pointers are discouraged in C++, and references are preferred. Yet, I cannot make a vector of references.

I wish to use only the standard libraries, so boost is off limits to me.

I heard of smart pointers. However, it appears as if there are multiple types of smart pointers. Would it not be overkill for this purpose? If smart pointers are indeed the answer, then how do I determine which one to use?

So my question is: What is the standard practice for creating a vector of references/pointers to objects?

In other words, would like the below (pseudo-)code to work.

#include <iostream>
#include <cstdlib>
#include <vector>

using namespace std;

class Object 
{
public:
    int field;
};

vector<Object> addToVector(Object &o)
{
    vector<Object> v;
    v.push_back(o);
    v[0].field = 3; // I want this to set o.field to 3.

    return v;
}

int main()
{
    Object one;
    one.field = 1;
    cout << one.field << endl; // 1 as expected

    Object &refone = one;
    refone.field = 2;
    cout << one.field << endl; // 2 as expected

    vector<Object> v = addToVector(one);

    cout << v[0].field << endl; // 3 as expected

    cout << one.field << endl; // I want to get 3 here as well, as opposed to 2.


    return 0;
}
like image 367
Zsw Avatar asked Nov 09 '22 06:11

Zsw


1 Answers

I would like to create an object, put the object into a vector, and still be able to modify the same object by accessing only the vector. However, I understand that when an object is push_back() to a vector, the object is actually copied into the vector. As a result, accessing the object in the vector will merely access a similar, but different object.

I'm almost certain that this is not what you want or "should" want. Forgive me that direct opening of my answer, but unless you have a very good reason to do this, you probably don't want to do it.

For that - a vector with references - to work you must guarantee that the referenced objects won't get moved nor destructed while you hold references to them. If you have them in a vector, make sure that vector isn't resized. If you have them on the stack like in your example, then don't let the vector of references or a copy of it leave that stack frame. If you want to store them in some container, use a std::list (it's iterators - pointers basically - don't get invalidated when inserting or removing elements).

You already noticed that you cannot have a vector of "real" references. The reason therefore is that references aren't assignable. Consider following code:

int a = 42;
int b = 21;
int & x = a; // initialisation only way to bind to something
int & y = b;
x = y;
b = 0;

After that, the value you obtain from x will be 21, because the assignment didn't change the reference (to be bound to b) but the referenced object, a. But a std::vector explicitly requires this.

You could now set out and write an wrapper around a pointer like ...

template<typename T>
struct my_ref {
  T * target;
  // don't let one construct a my_ref without valid object to reference to
  my_ref(T & t) : target(&t) {}
  // implicit conversion into an real reference
  operator T &(void) {
    return *target;
  }
  // default assignment works as expected with pointers
  my_ref & operator=(my_ref const &) = default;
  // a moved from reference doesn't make sense, it would be invalid
  my_ref & operator=(my_ref &&) = delete;
  my_ref(my_ref &&) = delete;
  // ...
};

... but this is pretty pointless since std::reference_wrapper already provides exactly that:

int main (int, char**) {
  int object = 21; // half of the answer
  vector<reference_wrapper<int>> v;
  v.push_back(object);
  v[0].get() = 42; // assignment needs explicit conversion of lhs to a real reference
  cout << "the answer is " << object << endl;
  return 0;
}

(Example live here)

Now one could argue why using a wrapper around a pointer like std::reference_wrapper when one could also directly use a pointer. IMO a pointer, having the ability to be nullptr, changes the semantics of the code: When you have a raw pointer, it could be invalid. Sure, you can just assume that it's not, or put it somewhere in comments, but in the end you then rely on something that's not guaranteed by the code (and this behaviour normally leads to bugs).

If an element of your vector could "reference" an object or be invalid, then still raw pointers aren't the first choice (for me): When you use an element from your vector which is valid, then the object referenced by it is actually referenced from multiple places on your code; it's shared. The "main" reference to the object then should be a std::shared_ptr and the elements of your vector std::weak_ptrs. You can then (thread safe) acquire a valid "reference" (a shared pointer) when you need to and drop it when done:

auto object = make_shared<int>(42);
vector<weak_ptr<int>> v;
v.push_back (object);

// ... somewhere later, potentially on a different thread
if (auto ref = v[0].lock()) {
  // noone "steals" the object now while it's used here
}
// let them do what they want with the object, we're done with it ...

Finally, please take my answer with a grain of salt, much of it is based on my opinion (and experience) and might not count as "standard practice".

like image 135
Daniel Jour Avatar answered Nov 15 '22 06:11

Daniel Jour