четверг, февраля 25, 2016

Несколько интересных свойств умных указателей в C++

Об этом полезно знать при использовании умных указателей в С++:

У shared_ptr есть конструктор, который позволяет создавать зависимости между shared_ptr'ами. (std::shared_ptr's secret constructor  by Anthony Williams)
Допустим, я хочу создать указатель на объект типа Y, py, который является членом экземпляра класса X, px. Мне нужно, чтобы px не удалялся, пока я не закончу работать с py.


void bar(){
    std::shared_ptr<X> px(std::make_shared<X>());
    std::shared_ptr<Y> py(px,&px->y);
    store_for_later(py);
} // our X object is kept alive



unique_ptr не такой уж и уникальный. (unique_ptr–How Unique is it? by Bartosz Milewski)

void f(Foo * pf) {
    globalFoo = pf; // creates a global alias
}

unique_ptr<Foo> pFoo(new Foo());
f(pFoo.get()); // leaks an alias


Передача shared_ptr по значению - дорогое удовольствие. (The Real Price of Shared Pointers in C++ by Nico Josuttis)
При передаче по значению происходит его копирование и к его внутреннему счетчику прибавляется единица, это нужно сделать атомарно, что влияет на производительность. Передавайте его по ссылке, где возможно.


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


Чтобы корректно возвращать shared_ptr на this надо использовать enable_shared_from_this. (std::enable_shared_from_this на cppreference.com)
Пример с  cppreference.com демонстрирует что случится, если вы не используете enable_shared_from_this.


#include <memory>
#include <iostream>

struct Good: std::enable_shared_from_this<Good>
{
    std::shared_ptr<Good> getptr() {
        return shared_from_this();
    }
};
 
struct Bad
{
    std::shared_ptr<Bad> getptr() {
        return std::shared_ptr<Bad>(this);
    }
    ~Bad() { std::cout << "Bad::~Bad() called\n"; }
};
 
int main()
{
    // Good: the two shared_ptr's share the same object
    std::shared_ptr<Good> gp1(new Good);
    std::shared_ptr<Good> gp2 = gp1->getptr();
    std::cout << "gp2.use_count() = " << gp2.use_count() << '\n';
 
    // Bad, each shared_ptr thinks it's the only owner of the object
    std::shared_ptr<Bad> bp1(new Bad);
    std::shared_ptr<Bad> bp2 = bp1->getptr();
    std::cout << "bp2.use_count() = " << bp2.use_count() << '\n';
} // UB: double-delete of Bad


Бонусная ссылка:
Smart Pointer Parameters by Herb Sutter

25 комментариев:

  1. Если я не ошибаюсь, при копировании shared_ptr увеличение счетчика делается атомарным инкрементом (interlocked increment в VC и я не помню уже какой интринсик в gcc). Возможно на каких-то платформах это в самом деле через мьютекс сделано, но я пока таких не видел.

    ОтветитьУдалить
  2. Vadim Panin
    Если я не ошибаюсь, при копировании shared_ptr увеличение счетчика делается атомарным инкрементом

    Ага, мне уже об этом писали, я поправила.

    ОтветитьУдалить
  3. Этот комментарий был удален автором.

    ОтветитьУдалить
  4. Зачем передавать const shared_ptr<T>, если можно передать T*?

    ОтветитьУдалить
    Ответы
    1. Почему нельзя? Можно.
      Вопрос баланса по стоимости.

      Удалить
  5. Привет, Атомарные операции дороже неатомарных?

    ОтветитьУдалить
  6. Зачем передавать const shared_ptr, если можно передать T*?

    Почему бы и не T& (с проверкой на nullptr где необходимо). Избегает сырых указателей и потенциальных проблем с их сохранением или освобождением.
    Для функций обработки данных, которые не влияют на владение объектами, не должно быть принципиально откуда берется объект: указатель, локальная переменная, контейнер, умный указатель, и т.п. Ссылки выглядят наиболее подходящим вариантом, за исключением, опять же, потенциальных nullptr. Передачу shared_ptr в аргументе стоит интерпретировать как явное разделение владения объектом. Аналогично для unique_ptr, его передача должна означать передачу владения.

    ОтветитьУдалить
  7. Этот комментарий был удален автором.

    ОтветитьУдалить
  8. А как на счет enable_shared_from_this<>, если Good не находится в shared_ptr<>?

    Good g1;
    std::shared_ptr<Good> gp2 = g1.getptr(); // всё ли тут хорошо?

    ОтветитьУдалить
  9. 2Den Raskovalov

    Зачем передавать const shared_ptr<T>, если можно передать T*?

    Чтобы у указателя стало на одного владельца больше.(вижу, что Alexander уже на это ответил).

    Также можно передать const shared_ptr<T>&, если ты не уверен нужен тебе еще один владелец или нет, по ходу функции можно передумать и присвоить его.

    2Alexander Batishchev

    Привет, Атомарные операции дороже неатомарных?

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

    ОтветитьУдалить
  10. 2Sergey Vostrikov

    А как на счет enable_shared_from_this<>, если Good не находится в shared_ptr<>?

    Good g1;
    std::shared_ptr<Good> gp2 = g1.getptr(); // всё ли тут хорошо?


    Не хорошо. Наличие хотя бы одно shared_ptr - обязательное условие

    std::enable_shared_from_this allows an object t that is currently managed by a std::shared_ptr named pt to safely generate additional std::shared_ptr instances

    ОтветитьУдалить
  11. Анонимный13/3/16 21:28

    Алена, что вы думаете по поводу языка Rust, есть смысл его изучать? Знаний по с++ нету.

    ОтветитьУдалить
  12. Анонимный

    Алена, что вы думаете по поводу языка Rust, есть смысл его изучать? Знаний по с++ нету.

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

    ОтветитьУдалить
  13. Жалко в РАДИО-Т больше не приходите...

    ОтветитьУдалить
  14. Volos_86
    Жалко в РАДИО-Т больше не приходите...

    Зато я недавно была в cppcast.

    ОтветитьУдалить
  15. Анонимный14/6/16 19:17

    Алена, а вы уже не в геймдеве?

    ОтветитьУдалить
  16. Анонимный
    Алена, а вы уже не в геймдеве?

    Я уже давно не в геймдеве.

    ОтветитьУдалить
  17. Я уже давно не в геймдеве.

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

    ОтветитьУдалить
  18. AlixBZ
    Простите за возможную бестактность с моей стороны, но с чем связана смена предметной области - с личными предпочтениями или с положением дел в игрострое?

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

    ОтветитьУдалить
  19. Эх хороший язык с++, жаль что давно забросила

    ОтветитьУдалить
  20. Анонимный23/11/16 16:57

    > shared_ptr "тяжелее" чем scoped_ptr, потому что он гарантирует корректную работу в многопоточных программах

    но есть ньюанс:
    https://habrahabr.ru/post/311560/

    надо батарейки к нему не забыть:
    http://en.cppreference.com/w/cpp/memory/shared_ptr/atomic

    ОтветитьУдалить
  21. Алексей13/9/17 13:46

    ./a.out
    gp2.use_count() = 2
    bp2.use_count() = 1
    Bad::~Bad() called
    Bad::~Bad() called

    Не понятно почему деструктор Bad был вызван после вывода bp2.use_count() оба раза, а не один раз до и один после.

    ОтветитьУдалить
  22. Как насчет intrusive_ptr из boost. Стратегия встроенного счетчика ссылок в текущем стандарте библиотеки не реализуется, и поэтому нужен weak_ptr поверх shared_ptr. std::make_shared конечно позволяет разместить указатели на счетчик ссылок и на объект в одном блоке памяти, и решить проблему (увеличив потребление памяти, на разного рода this). Однако пользователь API как правило понятия не имеет использовался-ли этот подход или нет (особенно если детали реализации неизвестны, есть только header файл) и вынужден использовать weak_ptr или вообще извлечь "сырой" указатель из интеллектуального. BWT, его легко извлечь из boost например вот так https://github.com/incoder1/IO/blob/master/include/config/libs/intrusive_ptr.hpp и не тащить с собой весь Boost.

    ОтветитьУдалить
  23. Анонимный15/8/18 18:33

    Когда вернётесь в геймдев?

    ОтветитьУдалить
  24. Когда вернётесь в геймдев?

    Пока не собираюсь

    ОтветитьУдалить