C ++ с использованием scoped_ptr в качестве переменной-члена - PullRequest
33 голосов
/ 01 февраля 2009

Просто хотел мнения по вопросу дизайна. Если у вас есть класс C ++, который владеет другими объектами, вы бы использовали для этого умные указатели?

class Example {
public: 
  // ...

private:
  boost::scoped_ptr<Owned> data;
};

Объект 'Owned' не может быть сохранен по значению, поскольку он может изменяться в течение всего времени существования объекта.

Я считаю, что, с одной стороны, вы проясняете, что объект принадлежит, и гарантируете его удаление, но, с другой стороны, вы можете просто иметь обычный указатель и удалить его в деструкторе. Это перебор?

Продолжение: Просто хотел сказать спасибо за все ваши ответы. Спасибо за сообщение о том, что auto_ptr оставил другой объект с указателем NULL, когда весь объект копируется, я широко использовал auto_ptr, но пока не думал об этом. Я делаю в основном все мои классы boost :: noncopyable, если у меня нет веских причин, так что мне не о чем беспокоиться. И спасибо также за информацию об утечках памяти в исключениях, это тоже полезно знать. Я стараюсь не писать вещи, которые могут вызвать исключения в конструкторе - есть лучшие способы сделать это, так что это не должно быть проблемой.

У меня только что был другой вопрос. Когда я задал этот вопрос, я хотел знать, действительно ли кто-то делал это, и вы все, кажется, упоминали, что это хорошая идея теоретически, но никто не сказал, что на самом деле это делают. Что меня удивляет! Конечно, один объект, имеющий указатель на другой, не является новой идеей, я бы ожидал, что вы все сделаете это раньше в какой-то момент. Что происходит?

Ответы [ 7 ]

40 голосов
/ 01 февраля 2009

scoped_ptr очень хорош для этой цели. Но нужно понимать его семантику. Вы можете группировать умные указатели, используя два основных свойства:

  • Копируемый: Интеллектуальный указатель может быть скопирован: Копия и исходный общий ресурс.
  • Подвижный: умный указатель может быть перемещен: у результата перемещения будет владение, у оригинала больше не будет.

Это довольно распространенная терминология. Для умных указателей существует специальная терминология, которая лучше отмечает эти свойства:

  • Передача права собственности: умный указатель - Подвижный
  • Доля владения: интеллектуальный указатель можно копировать. Если интеллектуальный указатель уже копируемый, то легко поддерживать семантику передачи права собственности: тогда это просто атомарная операция копирование и сброс оригинала , ограничивающая это интеллектуальными указателями определенных видов (например, только временные умные указатели).

Давайте сгруппируем доступные умные указатели, используя (C)opyable и (M)ovable, (N)either:

  1. boost::scoped_ptr: N
  2. std::auto_ptr: M
  3. boost::shared_ptr: C

auto_ptr имеет одну большую проблему в том, что он реализует концепцию Movable, используя конструктор копирования. Это связано с тем, что когда auto_ptr был принят в C ++, еще не было способа изначально поддерживать семантику перемещения с помощью конструктора перемещения, в отличие от нового стандарта C ++. То есть вы можете сделать следующее с auto_ptr, и оно работает:

auto_ptr<int> a(new int), b;
// oops, after this, a is reset. But a copy was desired!
// it does the copy&reset-of-original, but it's not restricted to only temporary
// auto_ptrs (so, not to ones that are returned from functions, for example).
b = a; 

В любом случае, как мы видим, в вашем случае вы не сможете передать право собственности на другой объект: ваш объект фактически не подлежит копированию. А в следующем стандарте C ++ он будет неподвижным, если вы останетесь с scoped_ptr.

Для реализации вашего класса с помощью scoped_ptr, обратите внимание, что вы удовлетворяете одному из следующих двух пунктов:

  • Написать деструктор (даже если он пустой) в файл .cpp вашего класса или
  • Сделать Owned полностью определенным классом.

В противном случае, когда вы создадите объект Example, компилятор неявно определит для вас деструктор, который вызовет деструктор scoped_ptr:

~Example() { ptr.~scoped_ptr<Owned>(); }

Затем будет вызван вызов scoped_ptr boost::checked_delete, который будет жаловаться на то, что Owned будет неполным, если вы не выполнили ни одного из двух вышеуказанных пунктов. Если вы определили свой собственный dtor в файле .cpp, неявный вызов деструктора scoped_ptr будет сделан из файла .cpp, в который вы можете поместить определение вашего Owned класса.

У вас та же проблема с auto_ptr, но у вас есть еще одна проблема: предоставление auto_ptr с неполным типом в настоящее время является неопределенным поведением (возможно, оно будет исправлено для следующей версии C ++). Итак, когда вы используете auto_ptr, у вас есть , чтобы сделать Owned полным типом в вашем заголовочном файле.

shared_ptr не имеет этой проблемы, потому что он использует полиморфное средство удаления, которое выполняет косвенный вызов удаления. Таким образом, функция удаления создается не в момент создания деструктора, а в момент создания средства удаления в конструкторе shared_ptr.

29 голосов
/ 01 февраля 2009

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

Вы должны помнить, что scoped_ptr не может быть скопирован, что делает ваш класс не копируемым по умолчанию до тех пор, пока вы не добавите свой собственный конструктор копирования и т. Д. нет-нет тоже!)

Если ваш класс имеет более одного поля указателя, то использование scoped_ptr фактически повышает безопасность исключений в одном случае:

class C
{
  Owned * o1;
  Owned * o2;

public:
  C() : o1(new Owned), o2(new Owned) {}
  ~C() { delete o1; delete o2;}
};

Теперь представьте, что во время создания C второй «новый владелец» выдает исключение (например, нехватка памяти). o1 будет пропущен, потому что C :: ~ C () (деструктор) не будет вызван, потому что объект еще не был полностью построен. Деструктор любого полностью построенного поля-члена действительно вызывается . Таким образом, использование scoped_ptr вместо простого указателя позволит правильно уничтожить o1.

8 голосов
/ 01 февраля 2009

Это вовсе не излишество, это хорошая идея.

Требуется, чтобы ваши классные клиенты знали о повышении. Это может или не может быть проблемой. Для переносимости вы можете рассмотреть std :: auto_ptr, который выполняет (в данном случае) ту же работу. Поскольку это личное, вам не нужно беспокоиться о том, что другие люди пытаются его скопировать.

5 голосов
/ 01 февраля 2009

Хорошая идея - использовать scoped_ptr.

Удерживать и уничтожать указатель вручную не так просто, как вы думаете. Особенно, если в вашем коде более одного указателя RAW. Если безопасность исключений и отсутствие утечек памяти является приоритетом, вам потребуется много дополнительного кода, чтобы исправить это.

Для начала вы должны убедиться, что правильно определили все четыре метода по умолчанию. Это связано с тем, что сгенерированная компилятором версия этих методов подходит для обычных объектов (включая интеллектуальные указатели), но в обычном случае приведет к проблемам с обработкой указателей (найдите проблему с мелким копированием).

  • Конструктор по умолчанию
  • Конструктор копирования
  • Оператор присваивания
  • Destructor

Если вы используете scoped_ptr, вам не нужно беспокоиться об этом.

Теперь, если у вас есть более одного указателя RAW в вашем классе (или другие части вашего конструктора могут генерировать). Вы должны ЯВНО иметь дело с исключениями во время строительства и разрушения.

class MyClass
{
    public:
        MyClass();
        MyClass(MyClass const& copy);
        MyClass& operator=(MyClass const& copy);
        ~MyClass();

    private
        Data*    d1;
        Data*    d2;
};

MyClass::MyClass()
    :d1(NULL),d2(NULL)
{
    // This is the most trivial case I can think off
    // But even it looks ugly. Remember the destructor is NOT called
    // unless the constructor completes (without exceptions) but if an
    // exception is thrown then all fully constructed object will be
    // destroyed via there destructor. But pointers don't have destructors.
    try
    {
        d1 = new Data;
        d2 = new Data;
    }
    catch(...)
    {
        delete d1;
        delete d2;
        throw;
    }
}

Посмотрите, насколько проще scopted_ptr.

4 голосов
/ 01 февраля 2009

Я не думаю, что это излишне, это гораздо лучше документирует семантику члена, чем необработанный указатель, и менее подвержено ошибкам.

4 голосов
/ 01 февраля 2009

Указатели Scoped хороши именно для этого, потому что они гарантируют, что объекты будут удалены без необходимости беспокоиться об этом как программист. Я думаю, что это хорошее использование ptr.

Я считаю, что в целом хорошая стратегия проектирования состоит в том, чтобы максимально избегать освобождения памяти и позволить вашим инструментам (в данном случае умным указателям) сделать это за вас. Ручное удаление плохо по одной основной причине, насколько я понимаю, и заключается в том, что код становится трудно поддерживать очень быстро. Логика выделения и освобождения памяти в коде часто является отдельной, и это приводит к тому, что дополнительные строки не поддерживаются вместе.

3 голосов
/ 01 февраля 2009

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

scoped_ptr хорош - просто используйте его:)

...