1
votes

Why does the inner elements of the vector are copied when the vector is passed by value?

#include<vector>
using namespace std;

// this func won't modify v[2]. 
// Meaning v[2] (and the whole inner array) was copied 
// when v is passed to the func?
void modify(vector<int> v) {
    v[2] = 100;
}

// this func modify v[2]
void modify(vector<int>& v) {
    v[2] = 100;
}

int main() {
    vector<int> v = {1,2,3,4,5,6};

    // still same
    modify(v);

    // modified
    modified2(v);
}

I find that it's strange that the actual content of the vector is copied when the vector is passed by value. I picture that the std::vector implementation must have a pointer field which maps to an address on heap, where the actual array is located. So when the vector is passed, even by value, the address should stay the same, pointing to the same content. Something like this:

#include<iostream>

using namespace std;

// a dummy wrapper of an array
// trying to mock vector<int>
class vector_int {
    public:
    int* inner_array; // the actual array 
    vector_int(int *a) {
        inner_array = a;
    }
    int* at(int pos) {
        return inner_array+pos;
    }
};

// this passes the "mocked vector" by value
// but 'inner_array' is not copied
void modify(vector_int v) {
    *(v.at(2)) = 10;
}

int main() {
    int* a = new int[3] {1,2,3};
    vector_int v = vector_int(a);
    modify(v); // v[2] is modified
}

Is this assumption about the std::vector implementation correct? What makes the vector content being copied when passed by value?


EDIT

Thanks to alter igel's answer and UnholySheep's comment, I figured out the reason why std::vector has value sementics (or why the inner array got copied).

If the copy constructor class is defined explicitly in the class definition, the copy constructor will determine how the struct/class instance is copied when the variable is passed in a function call. So I can define a copy constructor to my vector_int, in which I copy the whole inner_array, like

#include<iostream>

using namespace std;

class vector_int {
    public:
    int* inner_array;
    int len;
    vector_int(int *a, int len) {
        inner_array = a;
        this->len = len;
    }
    int* at(int pos) {
        return inner_array+pos;
    }
    // this is the copy constructor
    vector_int(const vector_int &v2) {
        inner_array = new int;
        for (int i =0; i < v2.len; i++) {
            *(inner_array+i) = *(v2.inner_array+i);
        }
    } 
};

// Yay, the vector_int's inner_array is copied
// when this function is called
// and no modification of the original vector is done
void modify(vector_int v) {
    *(v.at(2)) = 10;
}

int main() {
    int* a = new int[3] {1,2,3};
    vector_int v = vector_int(a,3);
    // 
    modify(v);
}

I checked the source code of the stdlib implementation on my local computer (g++ Apple LLVM version 10.0.0). The std::vector defines a copy constructor which looks like this

template <class _Tp, class _Allocator>
vector<_Tp, _Allocator>::vector(const vector& __x)
    : __base(__alloc_traits::select_on_container_copy_construction(__x.__alloc()))
{
    size_type __n = __x.size();
    if (__n > 0)
    {
        allocate(__n);
        __construct_at_end(__x.__begin_, __x.__end_, __n);
    }
}

which looks like it does an malloc for the actual copied array + copy the array.

2
The copy constructor makes sure it is copied. Also your implementation forgets one crucial detail: the destructor (and the rule of 3/5) - UnholySheep
All C++ standard containers act like value types. - melpomene
To define a function signature guaranteeing not to copy a std::vector<T> and not copy T's in turn use a const reference: void foo(const std::vector<T>& v); - πάντα ῥεῖ
Value semantics dictates that when you copy a value, the copy is independent of the original. For std::vector to have value semantics, copying it implies copying it's elements. There exists container implementations which perform copy-on-write but this is not practical for std::vector. And this approach loses a lot of it's benefits since the introduction of move semantics. If you are concerned about the cost of copying your vector then you likely don't mean to copy it and should read about reference types and move semantics instead. - François Andrieux
looks like that previously you were using langues where pass by reference is common default langue behavior (like Java or C#). In C structs and in C++ classes, when passed by value copy is made. There are some spatial cases where classes implement pattern COW to save memory. There are different approaches. Note that in functional programing you can't modify anything. To create modification you need to create copy with some variation. I'm just saying this is just langue design and there are different approaches, each have pros and cons. - Marek R

2 Answers

3
votes

C++ allows class types to provide their own code for what it means to be created, copied, moved, and destroyed, and that code is called implicitly, without any obvious function calls. This is called value semantics and it's what C++ uses where other languages resort to things like create_foo(foo), foo.clone(), destroy_foo(foo) or foo.dispose().

Each class can define the following special member functions:

  • Constructors, for putting the object into a valid initial state
  • A Destructor, for cleaning up responsibly
  • A Copy Constructor, for creating a new object that is a duplicate of another
  • A Move Constructor, for creating a new object by transferring the data of another
  • A Copy Assignment Operator, for duplicating an object into an existing object
  • A Move Assignment Operator, for transferring data between two existing objects

These are all functions that you can define to do whatever you want. But they are called implicitly, meaning that users of such a class don't see these function calls in their code, and they expect them to do predictable things. You should make sure that your classes behave predictably by following the Rule of Three/Five/Zero.

There are of course other tools for sharing data, like pass-by-reference, which you already know about.

Lots of classes in the standard library use these special member functions to implement special behaviors that are very useful and help users write safe, correct code. For example:

  • std::vector when copied, will always have identical elements, though the underlying array and objects contained will be separate.
  • std::unique_ptr wraps a resource that has only one owner. To enforce this, it can't be copied.
  • std::shared_ptr wraps a resource that has many owners. It's not totally clear when to clean up such a resource, so copying a shared_ptr performs automatic reference counting, and the resource is cleaned up only when the last owner is done with it.
1
votes

This is because vector has value semantics: when you copy it you get a true copy of all the elements.