Question

I have following code snippet

void func(int row,int n,std::vector<int>& buffer){
    if(row>n){
        return;
    }
    std::vector<int> new_buffer;
    for(int elm : buffer){
        std::cout<<elm<<" ";
        new_buffer.push_back(elm*2);
    }
    std::cout<<std::endl;
    func(row+1,n,new_buffer);
}

the thing is, I dont need the buffer varaible after the creation of new_buffer. And since I am continuing to recursively call the function after requirement of buffer is over, I am concerned that I am wasting space without reason. Is there any sure shot way of deallocating this space with compliance to the RAII scheme. If I call the destructor directly, the memory gets freed once again on the completion of the function resulting in a double free.

Was it helpful?

Solution

There is indeed a way to do that, but also likely a better way to approach the problem. I'll show both.

Add instrumentation

It's not always to tell when constructors and destructors are being called, so it's useful to add instrumentation. Here's some code I use sometimes that uses the Curiously Recurring Template Pattern, or CRTP for short. It isolates the reporting code into a templated class:

static int unsigned id{1};

template <typename T>
struct LeakDetector {
    LeakDetector() {
        serialnumber = id++;
        std::cout << "creating " << typeid(T).name() 
            << " " << serialnumber << '\n';
    }
    LeakDetector(const LeakDetector& other) {
        serialnumber = id++;
        std::cout << "creating " << typeid(T).name() 
            << " " << serialnumber << " via copy of "
            << typeid(other).name() 
            << " " << serialnumber << '\n';
    }
    ~LeakDetector() {
        std::cout << "deleting " << typeid(T).name() 
            << " " << serialnumber << '\n';
    }
private:
    unsigned serialnumber;
};

Now we create a new type which is essentially both that and a std::vector<int>:

struct MyVector : public LeakDetector<MyVector>, public std::vector<int>
{
    MyVector() = default;
    MyVector(std::initializer_list<int> m) : std::vector<int>{m} {}
    ~MyVector() = default;
};
    

Now we modify your function a bit:

using mytype = MyVector;

void func(int row,int n, mytype& buffer){
    if(row>n){
        return;
    }
    mytype new_buffer;
    for(int elm : buffer){
        std::cout<<elm<<" ";
        new_buffer.push_back(elm*2);
    }
    std::cout<<std::endl;
    func(row+1,n,new_buffer);
}

Now we try it out with a complete program:

#include <iostream>
#include <iterator>
#include <vector>
#include <algorithm>

// all the code above goes here
int main()
{
    mytype foo{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    std::cout << "\ncalling original func()\n";
    func(1, 3, foo);
    std::cout << "\nNow we're done with main()\n";
}

Here are the results of that on my machine:

creating 8MyVector 1

calling original func()
creating 8MyVector 2
1 2 3 4 5 6 7 8 9 10 
creating 8MyVector 3
2 4 6 8 10 12 14 16 18 20 
creating 8MyVector 4
4 8 12 16 20 24 28 32 36 40 
deleting 8MyVector 4
deleting 8MyVector 3
deleting 8MyVector 2

Now we're done with main()
deleting 8MyVector 1

Using move semantics

An alternative approach is to use move semantics which is just as it sounds; we move the object instead of just handing off a reference. Here's how that might look:

void func2(int row, int n, mytype&& buffer) {
    if(row > n) {
        return;
    }
    std::copy(buffer.begin(), buffer.end(), std::ostream_iterator<int>(std::cout, " "));
    std::cout << '\n';
    std::for_each(buffer.begin(), buffer.end(), [](int &x){x*=2;});
    func2(row+1, n, std::move(buffer));
}

Now we alter main slightly to demonstrate this version:

int main()
{
    mytype foo{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    std::cout << "\ncalling original func()\n";
    func(1, 3, foo);
    std::cout << "\ncalling move func()\n";
    func2(1, 3, std::move(foo));
    std::cout << "\nNow we're done with main()\n";
}

Results on my machine:

creating 8MyVector 1

calling original func()
creating 8MyVector 2
1 2 3 4 5 6 7 8 9 10 
creating 8MyVector 3
2 4 6 8 10 12 14 16 18 20 
creating 8MyVector 4
4 8 12 16 20 24 28 32 36 40 
deleting 8MyVector 4
deleting 8MyVector 3
deleting 8MyVector 2

calling move func()
1 2 3 4 5 6 7 8 9 10 
2 4 6 8 10 12 14 16 18 20 
4 8 12 16 20 24 28 32 36 40 

Now we're done with main()
deleting 8MyVector 1

An alternative

While this does what you've asked, there are better ways to do this. The main problem is that recursion tends to use more memory, but recursion can be replaced with iteration. Here's how that might look:

void func3(int row, int n, mytype buffer) {
    for( ; row <= n; ++row) {
        std::copy(buffer.begin(), buffer.end(), std::ostream_iterator<int>(std::cout, " "));
        std::cout << '\n';
        std::for_each(buffer.begin(), buffer.end(), [](int &x){x*=2;});
    }
}

Note that this time, we're passing by value, so func3 gets its own copy. This assures that it's completely self-contained and that even if the caller destroys its own copy before this function finishes (as could happen with multithreaded code), there is no problem within this function. Alternatively, one could pass a reference, but that means that the caller's copy is altered. Here's the updated main

int main()
{
    mytype foo{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    std::cout << "\ncalling original func()\n";
    func(1, 3, foo);
    std::cout << "\ncalling iterating func()\n";
    func3(1, 3, foo);
    std::cout << "\ncalling move func()\n";
    func2(1, 3, std::move(foo));
    std::cout << "\nNow we're done with main()\n";
}

And the results, once again:

creating 8MyVector 1

calling original func()
creating 8MyVector 2
1 2 3 4 5 6 7 8 9 10 
creating 8MyVector 3
2 4 6 8 10 12 14 16 18 20 
creating 8MyVector 4
4 8 12 16 20 24 28 32 36 40 
deleting 8MyVector 4
deleting 8MyVector 3
deleting 8MyVector 2

calling iterating func()
creating 8MyVector 5 via copy of 12LeakDetectorI8MyVectorE 5
1 2 3 4 5 6 7 8 9 10 
2 4 6 8 10 12 14 16 18 20 
4 8 12 16 20 24 28 32 36 40 
deleting 8MyVector 5

calling move func()
1 2 3 4 5 6 7 8 9 10 
2 4 6 8 10 12 14 16 18 20 
4 8 12 16 20 24 28 32 36 40 

Now we're done with main()
deleting 8MyVector 1
Licensed under: CC-BY-SA with attribution
scroll top