Search code examples
c++dictionarymemory-leakspolymorphismshared-ptr

Getting polymorphism to work in a C++ map without memory leaks?


It's been a long3 time since I programmed in C++. My polymorphism isn't working: the map<string, Base> converts my ArmyBase and NavyBase objects to Base objects when I add them to the map, so GetDescription() returns an empty string rather than the values I set via ArmyBase::SetDescription() and NavyBase::SetDescription(). Here's the extremely rough pseudo-code:

class Base
{ protected:
    string STR__Description;  // Pardon my non-standard style
  public:
    virtual
    string GetDescription()
    { return STR__Description;
    }
    void   SetDescription( string str__Description )
    { STR__Description = str__Description;
    }
}

class ArmyBase: public Base
{ public:
    string GetDescription()
    { return STR__Description + " (Army)";
    }
}

class NavyBase: public Base
{ public:
    string GetDescription()
    { return STR__Description + " (Navy)";
    }
}

It sounds like map<string, Base*> causes memory leaks and I'd rather not upgrade mid-project to use shared_ptr. Would storing the derived-class instances in a container that "destructs" them properly allow me to use the pointer map for polymorphism without risk of memory leakage?

Base                         base;
ArmyBase                     base_Army;
set<ArmyBase>                set__ArmyBases;
map<string, Base*>::iterator iter_Bases;
map<string, Base*>           map__Bases;
NavyBase                     base_Navy;
set<NavyBase>                set__NavyBases;
...
while( ... )
{   base_Army = ArmyBase();
    base_Navy = NavyBase();
    ...
    set__ArmyBases.insert(                                        base_Army   );
    map__Bases.insert(     pair<string, Base*>( "Boca Raton",    &base_Army ) );
    ...
    set__NavyBases.insert(                                        base_Navy   );
    map__Bases.insert(     pair<string> Base*>( "NAS Pensacola", &base_Navy ) );
    ...
    base = iter_Bases->second;
    std::cout << ..." " << base->GetDescription() << std::endl;
}

Desired output from map__Bases:

Boca Raton ... (Army)
NAS Pensacola ... (Navy)
...

Solution

  • The problem is that the map entries point to the adresses of the objects which you created on the stack. You copy those objects into your sets, but you don't store the addresses of the copies in your maps. When the original objects fall out of scope, they get deleted automatically and thus your map entries become invalid.

    I think the best way to solve your problem is to allocate your objects on the heap and store pointers in your containers. This, of course, requires you to be careful about memory management. I know three options to handle this:

    1. Manual memory management: Delete objects when you erase them from your container. This is dangerous and error-prone, of course, but with some care, you can make it work. I usually do this by wrapping the container in another class that will manage the objects, i.e., the wrapper has methods such as add(Base* base) and remove(Base* base), and it will also delete all objects in the container in its destructor. Of course, you must still take care not to delete such managed objects outside of the wrapper.

    2. Smart pointers: Use either shared_ptr or unique_ptr (depending on ownership semantics) and store those in the container. The smart pointers will take care of deleting the objects when they are removed from the container.

    3. Use a custom allocator. You can parametrize the std containers with allocators which should allow the container to delete the objects when they are removed from the map. However, I have never done this and can't comment on whether it's a good idea. I hear that writing custom allocators is quite difficult to get right.

    I would suggest either 1 and 2. I think it depends on taste and on other requirements which one you actually use, but if you use the smart pointer option, then make sure that you choose the smart pointer that models your particular ownership semantics (most likely unique_pointer).