3

A couple of months ago I asked this question where I asked why there was a memory leak. Apparently, I forgot a virtual destructor.

Now I'm struggling to understand why this is not a memory leak:

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


using namespace std;

class Base{
public:
    explicit Base(double a){
        a_ = a;
    }
    virtual void fun(){
        cout << "Base " << a_ << endl;
    }

protected:
    double a_;
};


class Derived : public Base{
public:
    Derived(double a, double b): Base(a), b_{b}{
    }
    void fun() override{
        cout << "Derived " << a_ << endl;
    }
private:
    double b_;
};



int main() {

    vector<unique_ptr<Base> > m;

    for(int i=0; i<10; ++i){
        if(i%2 == 0){
            m.emplace_back(make_unique<Base>(i));
        }else{
            m.emplace_back(make_unique<Derived>(i, 2*i));
        }
    }

    for(const auto &any:m){
        any->fun();
    }

    return 0;
}

Note that I do not have a virtual destructor for Base.

I thought that because I have a vector m of type unique_ptr<Base> only the destructor from Base gets called and my variable b_ in Derived would leak but according to valgrind this is not the case. Why is this not a memory leak?

I have tested this with valgrind-3.13.0

user7431005
  • 3,899
  • 4
  • 22
  • 49
  • May be of interest to know that `std::shared_ptr` will capture the type, and call the correct destructor. There is additional overhead using a `std::shared_ptr` rather than a `std::unique_ptr`. However, using the correct smart pointer really should be made so as to capture intent. Using the "wrong" one so as to get the type captured, all to avoid making the destructor virtual is going down a bad path (imo). – Eljay Nov 19 '18 at 13:12

3 Answers3

5

Deleting an object via a polymorphic pointer when the base class doesn't have a virtual destructor is undefined behaviour.

Undefined behaviour may mean your code leaks memory, crashes or works perfectly.

In this case the runtime library presumably allocated a single block of memory for your object and is able to delete that block correctly even when it is pointed to by a pointer of a different type. This is probably true for most runtimes but there are no guarantees. E.g. when using malloc() and free() you don't need to supply the size of the malloc() to free(), the same is happening here.

If you had defined a destructor in Derived you would see that it is not being called.

Alan Birtles
  • 32,622
  • 4
  • 31
  • 60
3

There would be a memory leak if b was an object that had resources (memory, network...) because of undefined behavior.

Here, by chance, the derived destructor doesn't do anything and the memory for the object is freed properly (this time). But anything more than built-in/trivially destructible types could trigger a memory leak (I suggest you try a vector of size 10 for instance).

BTW, o is not used?

Matthieu Brucher
  • 21,634
  • 7
  • 38
  • 62
  • I don't think that UB can cause a memory leak reliably. And there are lots of types which are trivially destructible aside from built-in types. – felix Nov 19 '18 at 11:30
  • @Fair enough. That's why I mentioned resources in my first phrase, but wasn't clear enough afterwards. – Matthieu Brucher Nov 19 '18 at 11:34
1

It doesn't leak memory because of how your C++ implementation behaves, but it is undefined behavior and you should fix it.

It's not a memory leak in this case because...

  1. std::make_unique allocates using new:

    template<class T, class... Args> unique_ptr<T> make_unique(Args&&... args); [...]
    Returns: unique_­ptr<T>(new T(std::forward<Args>(args)...)).
    [unique.ptr.create]

  2. std::unique_ptr deallocates using the std::default_delete which uses operator delete.

It doesn't matter from the perspective of memory leaks that the types are different, because delete will still be called with the pointer to the object allocated by new.

It would also be a memory leak if Derived did not have a trivial destructor. If it held a std::vector for example, that vector's destructor would be called by ~Derived. If it's not called, the storage for the vector would (detectably) leak.

See also: How does delete work?

However, all of this is undefined behavior per the C++ standard, so it might theoretically stop working any time. See [expr.delete/3].

In a single-object delete expression, if the static type of the object to be deleted is different from its dynamic type and the selected deallocation function (see below) is not a destroying operator delete, the static type shall be a base class of the dynamic type of the object to be deleted and the static type shall have a virtual destructor or the behavior is undefined.

palotasb
  • 4,108
  • 3
  • 24
  • 32