Каков наилучший способ реализации интеллектуальных указателей в C ++? - PullRequest
12 голосов
/ 02 февраля 2009

Я оценивал различные реализации интеллектуальных указателей (вау, там много), и мне кажется, что большинство из них можно разделить на две широкие классификации:

1) В этой категории используется наследование объектов, на которые имеются ссылки, так что они имеют счетчики ссылок и обычно реализуются up () и down () (или их эквивалентами). IE, чтобы использовать умный указатель, объекты, на которые вы указываете, должны наследоваться от некоторого класса, который предоставляет реализация ref.

2) В этой категории используется вторичный объект для хранения счетчиков ссылок. Например, вместо того, чтобы указывать умный указатель прямо на объект, он фактически указывает на этот объект метаданных ... У кого есть реализация счетчиков ссылок и up () и down () (и кто обычно предоставляет механизм для указателя на получить фактический объект, на который указывает указатель, чтобы умный указатель мог правильно реализовать оператор -> ()).

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

2 имеет проблему в том, что, поскольку счет хранится в другом объекте, если у вас когда-нибудь возникнет ситуация, когда указатель на существующий объект с подсчетом ссылок преобразуется в ссылку, у вас, вероятно, есть ошибка (IE, так как счетчик не находится в фактическом объекте, для новой ссылки нет способа получить счет ... ссылка на ссылку копирует конструкцию или назначение хорошо, потому что они могут совместно использовать объект счетчика, но если вам когда-либо придется конвертировать из указателя ты полностью отсталый) ...

Теперь, насколько я понимаю, boost :: shared_pointer использует механизм 2 или что-то вроде этого ... Тем не менее, я не могу определиться, что еще хуже! Я только когда-либо использовал механизм 1, в производственном коде ... Кто-нибудь имеет опыт работы с обоими стилями? Или, может быть, есть другой способ, который лучше, чем оба?

Ответы [ 9 ]

26 голосов
/ 02 февраля 2009

"Как лучше всего реализовать интеллектуальные указатели в C ++"

  1. Не надо! Используйте существующий, хорошо протестированный интеллектуальный указатель, такой как boost :: shared_ptr или std :: tr1 :: shared_ptr (std :: unique_ptr и std :: shared_ptr с C ++ 11)
  2. Если вам нужно, не забудьте:
    1. использовать идиому safe-bool
    2. предоставить оператору ->
    3. предоставить гарантию сильного исключения
    4. задокументируйте требования исключения, которые ваш класс предъявляет к удалителю
    5. по возможности используйте copy-modify-swap для реализации гарантии строгого исключения
    6. документ, правильно ли вы обрабатываете многопоточность
    7. написать подробные юнит-тесты
    8. реализует преобразование в базу таким образом, что оно будет удалять по производному типу указателя (умные указатели, запрошенные / динамические указатели динамического удаления)
    9. поддержка получения доступа к необработанному указателю
    10. Рассмотрим стоимость / выгоду от предоставления слабых указателей на разрывные циклы
    11. предоставьте соответствующие операторы приведения для ваших умных указателей
    12. сделать ваш конструктор шаблоном для обработки построения базового указателя из производного.

И не забывайте ничего, что я мог забыть в приведенном выше неполном списке.

9 голосов
/ 02 февраля 2009

Просто чтобы предоставить другое представление для вездесущего ответа Boost (хотя это правильный ответ для многих применений), взгляните на реализацию интеллектуальных указателей Loki . , Для обсуждения философии дизайна, оригинальный создатель Loki написал книгу Современный дизайн C ++ .

7 голосов
/ 02 февраля 2009

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

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

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

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

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

Стандартная библиотека имеет:

  • станд :: auto_ptr

У Boost на пару больше, чем было адаптировано tr1 (следующая версия стандарта)

  • станд :: tr1 :: shared_ptr
  • станд :: tr1 :: weak_ptr

И те, кто все еще в усилении (что в любом случае должно быть), которые, надеюсь, превратятся в tr2.

  • подталкивание :: scoped_ptr
  • повышение :: scoped_array
  • повышение :: shared_array
  • подталкивание :: intrusive_ptr

См: Умные указатели: или кому принадлежит ваш ребенок?

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

Boost также имеет навязчивый указатель (как решение 1), который не требует наследования от чего-либо. Это требует изменения указателя на класс для хранения счетчика ссылок и предоставления соответствующих функций-членов. Я использовал это в тех случаях, когда эффективность памяти была важна, и не хотел, чтобы для каждого используемого общего указателя использовались накладные расходы другого объекта.

Пример:

class Event {
public:
typedef boost::intrusive_ptr<Event> Ptr;
void addRef();
unsigned release();
\\ ...
private:
unsigned fRefCount;
};

inline void Event::addRef()
{
  fRefCount++;
}
inline unsigned Event::release(){
    fRefCount--;
    return fRefCount;
}

inline void intrusive_ptr_add_ref(Event* e)
{
  e->addRef();
}

inline void intrusive_ptr_release(Event* e)
{
  if (e->release() == 0)
  delete e;
}

Используется определение типа Ptr, так что я могу легко переключаться между boost :: shared_ptr <> и boost :: intrusive_ptr <> без изменения какого-либо клиентского кода

2 голосов
/ 02 февраля 2009

Мне кажется, что этот вопрос похож на вопрос "Какой алгоритм сортировки самый лучший?" Единого ответа нет, это зависит от ваших обстоятельств.

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

С другой стороны, для любого, кто может использовать TR1, я думаю, что тип 2 std :: tr1 :: shared_ptr будет разумным выбором по умолчанию, который будет использоваться всякий раз, когда нет какой-либо неотложной причины не используйте это.

1 голос
/ 02 февраля 2009

Вы говорите о навязчивых и неинтрузивных интеллектуальных указателях. Повышение имеет оба. boost::intrusive_ptr вызывает функцию для уменьшения и увеличения счетчика ссылок вашего объекта, каждый раз, когда ему нужно изменить счетчик ссылок. Это не вызов функций-членов, а бесплатные функции. Таким образом, это позволяет управлять объектами без необходимости изменять определение их типов. И, как вы говорите, boost::shared_ptr ненавязчив, ваша категория 2.

У меня есть ответ, объясняющий intrusive_ptr: Отключение shared_ptr для удаления . Короче говоря, вы используете его, если у вас есть объект, у которого уже есть счетчик ссылок, или вам нужен (как вы объясняете) объект, на который уже есть ссылка, чтобы он принадлежал intrusive_ptr.

1 голос
/ 02 февраля 2009

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

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

void doSomething (NIPtr<int> const &);

void foo () {
  NIPtr<int> i = new int;
  int & j = *i;
  doSomething (&j);          // Ooops - owned by two pointers! :(
}

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

К нашему большому удивлению, одна вещь, которую мы заметили, это то, что у нас было немедленное улучшение производительности с помощью навязчивого указателя. Мы не провели много исследований в этом, и просто предположили, что разница была в стоимости обслуживания объекта подсчета. Возможно, что другие реализации неинтрузивного общего указателя уже решили эту проблему.

1 голос
/ 02 февраля 2009

Проблему с 2 можно обойти. Boost предлагает boost :: shared_from_this по этой же причине. На практике это не большая проблема.

Но причина, по которой они выбрали ваш вариант №2, заключается в том, что он может использоваться во всех случаях. Полагаться на наследование не всегда возможно, и тогда у вас остается умный указатель, который вы не можете использовать для половины своего кода.

Я бы сказал, что №2 лучше, просто потому, что его можно использовать при любых обстоятельствах.

...