Just Let It Flow

February 5, 2010

Send in the Clones

Filed under: Wow, that was stupid — adeyblue @ 3:00 am

Boost’s ptr_containers are, for those who don’t know, STL like containers that deal solely with pointers that they take exclusive ownership of. Contrasted with a normal vector of smart pointers where the vector has a reference, while the tilemap has another and a third in the monster class, the ptr_containers act like containers of auto_ptr’s.

With this in mind, I employed one to ensure the pointers I added to it would be deleted on its destruction or if any of the intervening operations threw an exception. I plumbed it in and built around it and all was fine, that is until the unit tests ran and VC’s memory leak checking displayed a leak for every pointer I was adding to the container.

Much confused, since that was pretty much the whole point of what I was using it for, I set out to find why it was failing its’ basic duty. A bit of debugging later after paring down code to leave the effected area, and I found that the pointers in the container weren’t the ones that were passed in to push_back(). They were returned from the function just fine, but got mangled during insertion. But why?

Here’s some equivalent code that demonstrates the same problem to have yourself a little fun in figuring it out.

#include <iostream>
#include <boost/ptr_container/ptr_vector.hpp>
#include <boost/ptr_container/ptr_inserter.hpp>
#include <iterator>
#include <vector>
#include <algorithm>
#include <cstdlib>
 
int* doubleValue(int val)
{
    return new int(val * 2);
}
 
int main()
{
    std::vector<int> ints;
    std::generate_n(std::back_insert_iterator(ints), 15, &rand);
 
    boost::ptr_vector<int> intPtrs;
    std::transform(
        ints.begin(),
        ints.end(),
        boost::ptr_container::ptr_back_inserter(intPtrs),
        &DoubleValue
    );
    std::cout << "Original numbers\n";
    std::copy(ints.begin(), ints.end(), std::ostream_iterator<int>(std::cout, ", "));
    std::cout << "\nDoubled numbers\n";
    std::copy(intPtrs.begin(), intPtrs.end(), std::ostream_iterator<int>(std::cout, ", "));
    std::cout << std::endl;
}
 
/*
produces this x15 when run with VC++'s memory leak checking enabled:
app.cpp(10) : {206} client block at 0x00794DE8, subtype 0, 4 bytes long.
 Data: < M  > D2 4D 00 00 
*/

Whereas replacing the transform with the following manual version is perfectly fine:

for(size_t i = 0; i < ints.size(); ++i)
{
    intPtrs.push_back(doubleValue(ints[i]));
}

So why did the first version leak when it looks like it does exactly the same thing? It’s that concept-of-cloneability-that-is-plainly-visible-in-the-docs-but-you-don’t-really-pay-attention-to at work. When push_back is invoked by the ptr_back_inserter, it first clones the object using the containers clone allocator. In the vast majority of cases this is the default which makes a new copy of the passed in object on the heap. It’s then this copy which is then stored in the underlying container, the original pointer passed in is left untouched. So rather than being mangled as I originally though, it was just being cloned.

Of course, this is mentioned in the docs but as it was modelled after the standard library back_inserter I though it would behave the same and just pass its argument directly to push_back. Now, push_back does what I expected and simply adds the pointer to the container which is why the second version above works. Unfortunately, what was wrapped around it didn’t agree with my expectations. Just goes to show, avoid the documentation at your peril.

Powered by WordPress