NaOH
NaOH

Reputation: 148

Using std::vector from an object referenced by shared_ptr after shared_ptr's destruction

I apologise if the title is different from what I will be describing, I don't quite know how to describe it apart from using examples.

Suppose I have a shared_ptr of an object, and within that object, is a vector. I assign that vector to a variable so I can access it later on, and the shared_ptr gets destroyed as it goes out of scope. Question, is the vector I saved "safe" to access?

In the example below, from main(), outer() is called, and within outer(), inner() is called. inner() creates a shared_ptr to an object that contains a std::vector, and assigns it to a variable passed by reference. The role of outer() is to create some form of seperation, so that we know that the shared_ptr is destroyed. In main(), this referenced variable is accessed, but is it safe to use this variable?

#include <iostream>
#include <vector>
#include <memory>

struct sample_compound_obj {
    std::vector<int> vektor;
    sample_compound_obj(){std::cout << "I'm alive!"  << std::endl;};
    ~sample_compound_obj(){std::cout << "Goodbye, thank you forever!"  << std::endl;};
};

bool inner(std::vector<int>& input) {
    std::cout << "About to create sample_compound_obj..."  << std::endl;
    std::shared_ptr<sample_compound_obj> hehe(new sample_compound_obj);

    hehe->vektor.push_back(1);
    hehe->vektor.push_back(2);
    hehe->vektor.push_back(3);

    input = hehe->vektor;
    std::cout << "About to return from inner()..."  << std::endl;
    return true;
}

bool outer(std::vector<int>& input) {
    std::cout << "About to enter inner()..."  << std::endl;
    
    inner(input);

    std::cout << "About to return from outer()..."  << std::endl;

    return true;
}

int main() {
    std::cout << "About to enter outer()..."  << std::endl;
    std::vector<int> vector_to_populate;

    outer(vector_to_populate);

    for (std::vector<int>::iterator it = vector_to_populate.begin(); it != vector_to_populate.end(); it++) {
        std::cout << *it <<std::endl; // <-- is it even "safe" to access this vector
    }
}

https://godbolt.org/z/47EWfPGK3

To avoid XY problem, I first thought of this issue when I was writing some ROS code, where a subscriber callback passes by reference the incoming message as a const shared_ptr&, and the message contains a std::vector. In this callback, the std::vector is assigned (via =) to a global/member variable, to be used some time later, after the end of the callback, so presumably the original shared_ptr is destroyed. One big difference is that in my example, I passed the std::vector by reference between the functions, instead of a global variable, but I hope it does not alter the behavior. Question is, is the std::vector I have "saved", suitable to be used?

Upvotes: 0

Views: 183

Answers (3)

Ted Lyngmo
Ted Lyngmo

Reputation: 117298

is it safe to use this variable?

Yes, in the below statement, you copy the whole vector using the std::vector::operator= overload doing copy assignment. The two vectors do not share anything and live their separate lives and can be used independently of each other.

input = hehe->vektor;

Upvotes: 1

TopchetoEU
TopchetoEU

Reputation: 694

If you're familiar with the RAII concept and C++ references, you can pretty much skip the following explanations

In C++, vectors, and as a matter of fact, pretty much all structs, defined in std work very differently from what you might be used to in other higher level languages, like Java or C#. In C++, the RAII (resource acquisition is initialization) technique is used. I highly recommend that you actually read the article, but in short, it means that an object will define a constructor and a destructor, that allocate and free all memory used by the object, in your case, a std::vector, and the language is going to call the destructor when the object falls out of scope. This ensures that there are no memory leaks.

However, how would we go about passing a std::vector to a function, for example? Well, if we just made a straight up copy of the object, byte by byte, as we'd do in C, the function would run fine, until we reach the end of the function, and we call the destructor of the vector. In that case, after the function executes, when we go back to the caller, the vector is no longer valid, because its data got freed by the callee.

void callee(std::vector<int> vec) { }
void caller() {
    std::vector<int> vec;
    vec.push_back(10);
    callee(vec);
    vec.push_back(10); // This will break, with our current logic
}

Well, the keyword here is "copy". We copied the vector so that we can pass it in the callee function. We can solve this issue by creating custom copy behavior, in C++ terms that would be a copy constructor. In simple terms, a copy constructor takes an instance of the type itself as an argument, and copies it in the current instance. This allows us now to execute the code from above without any issues.

There are a lot more intricacies to it than I've written here, but other people have said it better than I have. In short, whenever you try to make an assignment, or pass an argument, you utilize the copy constructor (with some exceptions). In your case, you assign a vector variable with a vector value. This vector value gets copied, and so is valid until the function goes out of scope. There is a catch to that thou: if you try to modify the vector, you will modify only the copy, not the original. If you want to do so, you'll need to utilize references.

In C++, we have the concept of references. You can consider the reference something like a pointer, with some caveats to it. First of all, unlike pointers, you can't have a reference to a reference. The reference tells C++ that you don't work with the object itself, but an "alias" of the object. The object will exist in one place in the memory, but you will be able to access it in two places:

int a = 10;
int &ref_a = a;

std::cout << ref_a << ", " << a << "\n"; // 10, 10
ref_a = 5;
std::cout << ref_a << ", " << a << "\n"; // 5, 5
a = 8;
std::cout << ref_a << ", " << a << "\n"; // 8, 8

In the line int &ref_a = a, we don't actually copy a, but we tell ref_a that it is a reference of a. This means that any operations (including assignment) will be applied to a, not to ref_a. We can use references with variables, fields, return values and parameters. A reference is valid as long as the value it refers to is valid, so as soon as the value goes out of scope, the reference is no longer valid.

References can be used in parameters, in order to avoid copying the value. This can provide a lot of performance benefits, since we don't need to copy the value, but just pass a "pointer" to it. Of course, this means that if the function modifies the parameter, that is reflected in the caller:

void func(int &ref) {
   ref = 5;
}

void func2() {
    int a = 10;
    func(a);
    std::cout << a; // 5
}

TL; DR

In your case, you're returning a vector via an out parameter (reference parameter). Still, even if you're working with references, setting a reference will actually set the object behind the reference, and will use the copy constructor of the object. You can avoid that by making that a reference of a pointer to a vector, but working with pointers in C++ is strongly advised against. Regardless, the answer to your question is that this code is completely safe. Still, if you try to modify input in outer, you won't modify the vector in hehe, but instead you're going to modify the copy that inener has created.

Upvotes: 0

Aleksandr Medvedev
Aleksandr Medvedev

Reputation: 8978

In this case it's safe, because you get copy of the vector in this line:

input = hehe->vektor;

One big difference is that in my example, I passed the std::vector by reference between the functions, instead of a global variable, but I hope it does not alter the behavior.

Any reference can be bound only once, and in your scenario input reference is already bound to the argument passed (to be precise std::vector<int>& input of inner function is bound to std::vector<int>& input of outer function which itself is bound to std::vector<int> vector_to_populate). After a reference is bound, it acts as is object itself, so in the assignment statement you actually end up with calling something like this:

input.operator=(hehe->vektor);

Where operator= refers to the std::vector<T>::operator=(const std::vector<T> rhs) function.

Upvotes: 1

Related Questions