Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

stl container with std::unique_ptr's vs boost::ptr_container

With c++11 out there, I was asking myself if there is a replacement of the boost::ptr_containers in c++11. I know I can use e.g. a std::vector<std::unique_ptr<T> >, but I'm not sure if this is a complete replacement. What is the recommended way of handling these cases?

like image 278
P3trus Avatar asked Feb 27 '12 18:02

P3trus


People also ask

Why auto_ ptr cannot be used with STL?

The C++ Standard says that an STL element must be "copy-constructible" and "assignable." In other words, an element must be able to be assigned or copied and the two elements are logically independent. std::auto_ptr does not fulfill this requirement.

Are STL containers passed by reference?

@BjörnPollex Yes! I forgot to mention that.


2 Answers

I decided to write up a short program that put a few polymorphic objects into a container (by pointer to heap), and then use that container with a std::algorithm. I chose std::remove_if just as an example.

Here is how I would do it with vector<unique_ptr<T>>:

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

class Animal
{
public:
    Animal() = default;
    Animal(const Animal&) = delete;
    Animal& operator=(const Animal&) = delete;
    virtual ~Animal() = default;

    virtual void speak() const = 0;
};

class Cat
    : public Animal
{
public:
    virtual void speak() const {std::cout << "Meow\n";}
    virtual ~Cat() {std::cout << "destruct Cat\n";}
};

class Dog
    : public Animal
{
public:
    virtual void speak() const {std::cout << "Bark\n";}
    virtual ~Dog() {std::cout << "destruct Dog\n";}
};

class Sheep
    : public Animal
{
public:
    virtual void speak() const {std::cout << "Baa\n";}
    virtual ~Sheep() {std::cout << "destruct Sheep\n";}
};

int main()
{
    typedef std::unique_ptr<Animal> Ptr;
    std::vector<Ptr> v;
    v.push_back(Ptr(new Cat));
    v.push_back(Ptr(new Sheep));
    v.push_back(Ptr(new Dog));
    v.push_back(Ptr(new Sheep));
    v.push_back(Ptr(new Cat));
    v.push_back(Ptr(new Dog));
    for (auto const& p : v)
        p->speak();
    std::cout << "Remove all sheep\n";
    v.erase(
        std::remove_if(v.begin(), v.end(),
                       [](Ptr& p)
                           {return dynamic_cast<Sheep*>(p.get());}),
        v.end());
    for (auto const& p : v)
        p->speak();
}

This outputs:

Meow
Baa
Bark
Baa
Meow
Bark
Remove all sheep
destruct Sheep
destruct Sheep
Meow
Bark
Meow
Bark
destruct Dog
destruct Cat
destruct Dog
destruct Cat

which looks good to me. However I found translating this to ptr_vector problematic:

boost::ptr_vector<Animal> v;
v.push_back(new Cat);
v.push_back(new Sheep);
v.push_back(new Dog);
v.push_back(new Sheep);
v.push_back(new Cat);
v.push_back(new Dog);
for (auto const& p : v)
    p.speak();
std::cout << "Remove all sheep\n";
v.erase(
    std::remove_if(v.begin(), v.end(),
                   [](Animal& p)
                       {return dynamic_cast<Sheep*>(&p);}),
    v.end());
for (auto const& p : v)
    p.speak();

algorithm:1897:26: error: overload resolution selected deleted operator '='
                *__first = _VSTD::move(*__i);
                ~~~~~~~~ ^ ~~~~~~~~~~~~~~~~~
test.cpp:75:9: note: in instantiation of function template specialization 'std::__1::remove_if<boost::void_ptr_iterator<std::__1::__wrap_iter<void
      **>, Animal>, Sheep *(^)(Animal &)>' requested here
        std::remove_if(v.begin(), v.end(),
        ^
test.cpp:12:13: note: candidate function has been explicitly deleted
    Animal& operator=(const Animal&) = delete;
            ^
1 error generated.

The problem is a feature of boost::ptr_vector: the iterators don't return the internally stored pointers. They return the pointers dereferenced. And thus when the container is used with std::algorithms, the algorithms try to copy the stored objects instead of the stored pointers to the objects.

If one accidentally forgets to make your polymorphic objects non-copyable, then copy semantics are automatically supplied, resulting in a run time error instead of a compile time error:

class Animal
{
public:
    Animal() = default;
    virtual ~Animal() = default;

    virtual void speak() const = 0;
};

Which now results in this erroneous output:

Meow
Baa
Bark
Baa
Meow
Bark
Remove all sheep
destruct Cat
destruct Dog
Meow
Baa
Bark
Baa
destruct Cat
destruct Sheep
destruct Dog
destruct Sheep

This run time error can not happen when using vector<unique_ptr>.

The impedance mismatch of storing containers of pointers but presenting containers of references appears at odds with safe use of the containers with generic algorithms. Indeed, this is why the ptr_containers come with custom versions of many of the algorithms. The correct way to do this job with ptr_containers is to use only those member algorithms:

v.erase_if([](Animal& p)
                 {return dynamic_cast<Sheep*>(&p);});

If you need a mutating sequence algorithm not supplied as a member of the ptr_containers, do not be tempted to reach for those in <algorithm>, or those generic algorithms supplied by other 3rd parties.

In summary, the boost::ptr_containers filled a real need when the only other practical option was std::vector<boost::shared_ptr<T>>. However now with std::vector<std::unique_ptr<T>>, the overhead argument is gone. And there appears to be both safety and flexibility advantages with the C++11 solution. If you need "clone semantics", I would seriously consider writing your own clone_ptr<T> and using that with the std containers and algorithms.

Reuse of the std::lib will keep your options of containers more open than the boost lib (e.g. unordered_set/map, forward_list), and it will keep your options of std::algorithms open as wide as possible.

That being said, if you have working, debugged code already using the boost::ptr_containers, there's no urgent need to change it.

like image 129
Howard Hinnant Avatar answered Sep 17 '22 15:09

Howard Hinnant


They really solve two similar but different problems.

A pointer container is a way to store objects in a container that just so happen to be pointers to allocated memory rather than values. They do everything in their power to hide the fact that they are a container of pointers. This means:

  • Entries in the container cannot be NULL.
  • Values you get from iterators and functions are references to the type, not pointers to the type.
  • Working with many standard algorithms can be... tricky. And by "tricky", I mean broken. Pointer containers have their own built-in algorithms.

However, the fact that pointer containers know that they're containers of pointers, they can offer some new functionality:

  • A clone member function that performs a deep copy, via the use of a certain "Cloneable" concept on the type of the object.
  • The ability of a container to release ownership of its objects (after a shallow copy, for example).
  • Built-in functions to transfer ownership to other containers.

They really are quite different concepts. There is a lot of stuff you would have to do manually that pointer containers can do automatically with specialized functions.

If you really need a container of pointers, then you can use containers of unique_ptr. But if you need to store a bunch of objects that you happen to heap allocate, and you want to play special games with them involving ownership and such, then the pointer containers are not a bad idea.

like image 31
Nicol Bolas Avatar answered Sep 19 '22 15:09

Nicol Bolas