In what situations should I dynamically allocate an array in C++?

Question:

I'm working on a framework code for my work. In one of the functions, it dynamically allocates a 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;
}

That is, it is up to the user to liquidate 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 covers, correct?

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

Questions:

  1. Is this really a case where 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 in allocating a std::vector dynamically. 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, allow the compiler to apply return optimizations. In them the object to be returned is constructed directly in 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 inside 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 constructed at the return point itself, that is: a temporary (that 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 is not possible to apply these optimizations (I suggest rewriting so that the function is as in one of these examples), then there are two options left: move or copy the object, the first being very light and the second very expensive. Unfortunately there is no concept of moving in C++03 and if you cannot use C++11 you will have to use other means to avoid copying, such as 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 quite light 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 insert function call on the vector may fail and throw the std::bad_alloc exception in case of lack of memory. If this 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 unlocks it, 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 which 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 "gut instincts" can be risky. When faced with situations like this one thing to do is 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 the output 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 the 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 just before it actually returns from the function. So the compiler must first copy the result to the return address before proceeding. 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 perform 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