In what situations should I dynamically allocate a vector in C++?

Question:

I'm tinkering with a framework code for my work. In one of the functions, it dynamically allocates an std::vector , makes a copy of each node the object has, and returns it to the user:

std::vector<Node> *Tree::getAllNodes() {
    std::vector<Node> *n, *aux;
    n = new vector<Node>();
    mut.lock();
    for (int i = 0; i < subnodes.size(); ++i) {
        aux = subnodes[i]->GetNodes();
        n->insert(n->end(), aux->begin(), aux->end());
    }
    mut.unlock();
    return n;
}

In other words, it is up to the user to settle this memory later.

But, I don't know if it's really necessary to allocate this memory dynamically, since the vector takes care of that for us, under the hood, right?

One of the reasons I find is that it's cheaper to return just the pointer than the vector copy when we have a lot of data. If we didn't allocate it dynamically, we would have to return a copy, and because it's a lot of data, it would be more costly.

Questions:

  1. Is this really a case that we should dynamically allocate the vector ?
  2. In other cases, when we have little data and/or few calls to this function, is it unnecessary to do this dynamic allocation? After all, memory management gets simpler.

Answer:

I can't see any real advantage to dynamically allocating an std::vector . But care must be taken when returning a vector as a result of a function. Even though it is small (typically 12 bytes on 32-bit systems) its copy constructor is slow.

  • If possible, let the compiler apply the fallback optimizations. In them, the object to be returned is built directly into its destination variable after the function call. There are two possible modes (12.8/31):

    • NRVO (Named Return Value Optimization) : When you create a variable within the function whose type is the return type of the function and at all return points, you return that variable. Example:

       // Otimização de valor nomeado: // Todos os returns devem retornar a mesma variável local. std::vector<int> func_nrvo() { std::vector<int> result; // execute algo aqui e adicione elementos ao vetor return result; } std::vector<int> result = func_nrvo();
    • RVO (Return Value Optimization) : When you return an object built on the return point itself, that is: a temporary one (which is exactly the type of the function). Example:

       // Otimização de valor não nomeado: // Todos os returns devem ser um objeto temporário do tipo de retorno. std::vector<int> func_rvo() { return std::vector<int>(5, 0); } std::vector<int> result = func_rvo();
  • If it's not possible to apply these optimizations (I suggest rewriting it so that the function looks like in one of these examples), then there are two options: move or copy the object, the first being very light and the second very costly. Unfortunately there is no concept of moving in C++03 and if you can't use C++11 you will have to use other means to avoid copying, like using a reference argument to return:

     void func_ref(std::vector<int>& vec) { vec.clear(); vec.push_back(1); } std::vector<int> vec; func_ref(vec);
  • If you use a compiler that supports C++11:

    The vector in this case has a constructor that can move the object. So returning the vector by a function is pretty lightweight and you don't have to worry. In cases where return value optimization does not apply, but if you return a local variable, the result will be moved automatically. But you can force the move action using std::move if the situation is different.

     std::vector<int> func1() { return std::vector<int>({1, 2, 3, 4, 5}); } std::vector<int> func2() { std::vector<int> a, b; if (rand() % 2) return a; else return b; // Otimização não se aplica. Estamos retornando variáveis diferentes. // Mas ainda estamos retornando variáveis, mover é implícito. } std::vector<int> func3() { std::vector<int> a, b; return (rand() % 2) ? std::move(a) ? std::move(b); // Otimização não se aplica. Estamos retornando variáveis diferentes. // Mas note que não estamos retornando variáveis, e sim uma estrutura // mais complexa (a condicional ternária). Nesse caso precisa forçar o move. } std::vector<int> vec1 = func1(); // Resultado construído diretamente em vec1 std::vector<int> vec2 = func2(); // Resultado movido para vec2 std::vector<int> vec3 = func3(); // Resultado movido para vec3

note:

In the code of your function I see that you use a mutex. Note that the vector insert function call may fail and throw the std::bad_alloc exception in case of memory shortage. If that happens your function will be terminated without releasing the mutex. A deadlock waiting to arise!

Ideally, use a class whose constructor locks the mutex and the destructor std::lock_guard , such as std::lock_guard . So even in case of an exception the mutex will be released because the destructor of the local variables is always called.

In case of doubt…

The rules governing exactly what type of constructor to call, when, and what optimizations can be made in each case are quite complex, and analyzing code based solely on your "instincts" can be risky. When faced with situations like this one thing is to trust your compiler to tell you what's going on. Instead of a vector, use a "guinea pig" class to see through the code. Example:

struct Cobaia {
    Cobaia() {cout << "  Cobaia()" << endl;}
    ~Cobaia() {cout << "  ~Cobaia()" << endl;}
    Cobaia(const Cobaia&) {cout << "  Cobaia(const Cobaia&)" << endl;}
    Cobaia(Cobaia&&) {cout << "  Cobaia(Cobaia&&)" << endl;} // apenas C++11
};

volatile bool cond = true; // volatile para não otimizar

Cobaia func1() { Cobaia r; return r; }
Cobaia func2() { return Cobaia(); }
Cobaia func3() { Cobaia a, b; if (cond) return a; else return b; }
Cobaia func4() { Cobaia a, b; return cond ? a : b; }
Cobaia func5() { Cobaia a, b; return std::move(cond ? a : b); } // apenas C++11

int main() {
    cout << "func1:" << endl; Cobaia c1 = func1();
    cout << "func2:" << endl; Cobaia c2 = func2();
    cout << "func3:" << endl; Cobaia c3 = func3();
    cout << "func4:" << endl; Cobaia c4 = func4();
    cout << "func5:" << endl; Cobaia c5 = func5(); // apenas C++11
    cout << "fim:" << endl;
}

Here is the result of this program (compiled with GCC 4.8.1 in C++11 mode, my comments):

func1:
  Cobaia() // Otimização acontecendo. Tudo acontece como se 'c1' estivesse dentro de func1
func2:
  Cobaia() // Otimização acontecendo. Tudo acontece como se 'c2' estivesse dentro de func2
func3:
  Cobaia() // Construção do local a
  Cobaia() // Construção do local b
  Cobaia(Cobaia&&) // Mover a ou b para c3
  ~Cobaia() // Construção do local b
  ~Cobaia() // Construção do local a
func4:
  Cobaia() // Construção do local a
  Cobaia() // Construção do local b
  Cobaia(const Cobaia&) // Copiar a ou b para c4
  ~Cobaia() // Construção do local b
  ~Cobaia() // Construção do local a
func5:
  Cobaia() // Construção do local a
  Cobaia() // Construção do local b
  Cobaia(Cobaia&&) // Mover a ou b para c5
  ~Cobaia() // Construção do local b
  ~Cobaia() // Construção do local a
fim:
  ~Cobaia() // Destruir c5
  ~Cobaia() // Destruir c4
  ~Cobaia() // Destruir c3
  ~Cobaia() // Destruir c2
  ~Cobaia() // Destruir c1

Note the difference between func3 , func4 and func5 . It's a clear example of how obscure these rules can be. In func3 the return is a local variable, so it has a fixed address from which data can be moved . In the case of func4 the return expression is a temporary one, so it is expected that its value will be destroyed right before it actually returns from the function. So the compiler must first copy the result to the return address before continuing. In func5 use std::move to convert the expression into a Cobaia&& that can be moved .

If you run the same code in C++03 you will see that the latter make copies as there is no concept of moving an object.

For extra doses of fun, add the -fno-elide-constructors flag in GCC. It turns off all these optimizations and you can see each and every copy happening. An exercise: determine the reason for each.

Scroll to Top