Контейнер stl с std :: unique_ptr vs boost :: ptr_container - PullRequest
19 голосов
/ 27 февраля 2012

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

Ответы [ 2 ]

22 голосов
/ 28 февраля 2012

Я решил написать короткую программу, которая помещает несколько полиморфных объектов в контейнер (по указателю в кучу), а затем использую этот контейнер с помощью алгоритма 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.

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

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

20 голосов
/ 27 февраля 2012

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

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

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

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

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

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

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

...