Контейнер stl с std::unique_ptr против boost::ptr_container

С появлением C++11 я спрашивал себя, есть ли замена boost::ptr_containers в C++11. Я знаю, что могу использовать, например, std::vector<std::unique_ptr<T> >, но я не уверен, что это полная замена. Каков рекомендуемый способ обработки этих случаев?

2 ответа

Решение

Они действительно решают две похожие, но разные проблемы.

Контейнер указателя - это способ хранения объектов в контейнере, который, как оказалось, является указателем на выделенную память, а не на значения. Они делают все возможное, чтобы скрыть тот факт, что они являются контейнером указателей. Это означает:

  • Записи в контейнере не могут быть NULL.
  • Значения, которые вы получаете от итераторов и функций, являются ссылками на тип, а не указателями на тип.
  • Работать со многими стандартными алгоритмами может быть... сложно. И под "хитрым" я подразумеваю сломленный. Контейнеры-указатели имеют свои собственные встроенные алгоритмы.

Однако тот факт, что контейнеры указателей знают, что они являются контейнерами указателей, они могут предложить некоторые новые функции:

  • clone Функция-член, которая выполняет глубокое копирование, используя определенную концепцию "Клонируемый" для типа объекта.
  • Способность контейнера освобождать право собственности на его объекты (например, после мелкой копии).
  • Встроенные функции для передачи прав собственности на другие контейнеры.

Это действительно разные понятия. Есть много вещей, которые вы должны будете сделать вручную, что контейнеры указателей могут делать автоматически с помощью специализированных функций.

Если вам действительно нужен контейнер указателей, то вы можете использовать контейнеры unique_ptr, Но если вам нужно хранить кучу объектов, выделенных вам в куче, и вы хотите играть в специальные игры с участием владельцев и тому подобное, тогда контейнеры с указателями - неплохая идея.

Я решил написать короткую программу, которая помещает несколько полиморфных объектов в контейнер (указатель на кучу), а затем использует этот контейнер с алгоритмом std::. Я выбрал std::remove_if просто в качестве примера.

Вот как бы я это сделал 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();
}

Это выводит:

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

что выглядит хорошо для меня. Однако я нашел перевод этого ptr_vector проблематична:

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.

Проблема в том, что особенность boost::ptr_vector: итераторы не возвращают внутренне хранимые указатели. Они возвращают указатели разыменованными. И, таким образом, когда контейнер используется с std::algorithmsалгоритмы пытаются копировать сохраненные объекты вместо сохраненных указателей на объекты.

Если кто-то случайно забудет сделать ваши полиморфные объекты недоступными для копирования, семантика копирования будет предоставлена ​​автоматически, что приведет к ошибке времени выполнения вместо ошибки времени компиляции:

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

    virtual void speak() const = 0;
};

Что теперь приводит к этому ошибочному выводу:

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

Эта ошибка во время выполнения не может произойти при использовании vector<unique_ptr>,

Несоответствие импеданса при хранении контейнеров указателей, но при представлении контейнеров ссылок оказывается в противоречии с безопасным использованием контейнеров с общими алгоритмами. Именно поэтому ptr_containers поставляются с пользовательскими версиями многих алгоритмов. Правильный способ сделать эту работу с ptr_containers - это использовать только те алгоритмы-члены:

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

Если вам нужен алгоритм мутирующей последовательности, не предоставленный в качестве члена ptr_containers, не поддавайтесь искушению достичь тех, кто в <algorithm>или те общие алгоритмы, предоставленные другими третьими сторонами.

Таким образом, boost::ptr_containers полностью удовлетворял потребности, когда единственным другим практическим вариантом было std::vector<boost::shared_ptr<T>>, Однако теперь с std::vector<std::unique_ptr<T>>аргумент накладных расходов пропал. Похоже, что решение C++11 дает преимущества как в плане безопасности, так и в плане гибкости. Если вам нужна "семантика клонов", я бы серьезно подумал написать свою собственную clone_ptr<T> и используя это со стандартными контейнерами и алгоритмами.

Повторное использование std::lib сделает ваши параметры контейнеров более открытыми, чем boost lib (например, unordered_set/map, forward_list), и обеспечит максимально широкий доступ к вашим параметрам алгоритмов std::.

Тем не менее, если у вас есть рабочий отлаженный код, уже использующий boost::ptr_containers, его не нужно срочно менять.

Другие вопросы по тегам