четверг, февраля 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 коммент.:

Vadim Panin комментирует...

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

Alena комментирует...

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

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

Unknown комментирует...
Этот комментарий был удален автором.
Unknown комментирует...

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

Сергей комментирует...

Почему нельзя? Можно.
Вопрос баланса по стоимости.

abatishchev комментирует...

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

Alexander комментирует...

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

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

Unknown комментирует...
Этот комментарий был удален автором.
Unknown комментирует...

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

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

Alena комментирует...

2Den Raskovalov

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

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

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

2Alexander Batishchev

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

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

Alena комментирует...

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

Анонимный комментирует...

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

Alena комментирует...

Анонимный

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

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

Volos_86 комментирует...

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

Alena комментирует...

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

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

Анонимный комментирует...

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

Alena комментирует...

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

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

AlixBZ комментирует...

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

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

Alena комментирует...

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

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

Мария комментирует...

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

Анонимный комментирует...

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

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

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

Алексей комментирует...

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

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

Victor комментирует...

Как насчет 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.

Анонимный комментирует...

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

Alena комментирует...

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

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