Поделиться через


Практическое руководство. Создание и использование экземпляров shared_ptr

Тип shared_ptr — это умный указатель в стандартной библиотеке C++, предназначенной для сценариев, в которых несколько владельцев должны управлять временем существования объекта. После инициализации указателя shared_ptr его можно копировать, передавать по значению в аргументах функций и присваивать другим экземплярам shared_ptr. Все экземпляры указывают на один и тот же объект и имеют общий доступ к одному "блоку управления", который увеличивает и уменьшает счетчик ссылок, когда указатель shared_ptr добавляется, выходит из области действия или сбрасывается. Когда счетчик ссылок достигает нуля, блок управления удаляет ресурс в памяти и самого себя.

На схеме ниже показано несколько экземпляров shared_ptr, указывающих на одно расположение в памяти.

Схема с двумя shared_ptr экземплярами, указывающими на одно расположение памяти.

На первой схеме показан общий указатель P1, указывающий на экземпляр MyClass, а также блок управления с числом ссылок = 1. На второй схеме показано добавление другого общего указателя P2, который также указывает на экземпляр MyClass и общий блок управления, который теперь имеет число ссылок 2.

Пример настройки

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

// shared_ptr-examples.cpp
// The following examples assume these declarations:
#include <algorithm>
#include <iostream>
#include <memory>
#include <string>
#include <vector>

struct MediaAsset
{
    virtual ~MediaAsset() = default; // make it polymorphic
};

struct Song : public MediaAsset
{
    std::wstring artist;
    std::wstring title;
    Song(const std::wstring& artist_, const std::wstring& title_) :
        artist{ artist_ }, title{ title_ } {}
};

struct Photo : public MediaAsset
{
    std::wstring date;
    std::wstring location;
    std::wstring subject;
    Photo(
        const std::wstring& date_,
        const std::wstring& location_,
        const std::wstring& subject_) :
        date{ date_ }, location{ location_ }, subject{ subject_ } {}
};

using namespace std;

int main()
{
    // The examples go here, in order:
    // Example 1
    // Example 2
    // Example 3
    // Example 4
    // Example 6
}

Пример 1

По возможности используйте функцию make_shared для создания shared_ptr при создании ресурса памяти в первый раз. Функция make_shared безопасна в отношении исключений. Для выделения памяти под блок управления и ресурс используется один вызов, что снижает накладные расходы. Если вы не используете функцию make_shared, то придется использовать явное выражение new для создания объекта, прежде чем передавать его в конструктор shared_ptr. В приведенном ниже примере представлены различные способы объявления и инициализации указателя shared_ptr вместе с новым объектом.

// Use make_shared function when possible.
auto sp1 = make_shared<Song>(L"The Beatles", L"Im Happy Just to Dance With You");

// Ok, but slightly less efficient. 
// Note: Using new expression as constructor argument
// creates no named variable for other code to access.
shared_ptr<Song> sp2(new Song(L"Lady Gaga", L"Just Dance"));

// When initialization must be separate from declaration, e.g. class members, 
// initialize with nullptr to make your programming intent explicit.
shared_ptr<Song> sp5(nullptr);
//Equivalent to: shared_ptr<Song> sp5;
//...
sp5 = make_shared<Song>(L"Elton John", L"I'm Still Standing");

Пример 2

В следующем примере показано, как объявить и инициализировать shared_ptr экземпляры, которые принимают совместное владение объектом, выделенным другим shared_ptr. Предполагается, что sp2 — это инициализированный указатель shared_ptr.

//Initialize with copy constructor. Increments ref count.
auto sp3(sp2);

//Initialize via assignment. Increments ref count.
auto sp4 = sp2;

//Initialize with nullptr. sp7 is empty.
shared_ptr<Song> sp7(nullptr);

// Initialize with another shared_ptr. sp1 and sp2
// swap pointers as well as ref counts.
sp1.swap(sp2);

Пример 3

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

vector<shared_ptr<Song>> v {
  make_shared<Song>(L"Bob Dylan", L"The Times They Are A Changing"),
  make_shared<Song>(L"Aretha Franklin", L"Bridge Over Troubled Water"),
  make_shared<Song>(L"Thalía", L"Entre El Mar y Una Estrella")
};

vector<shared_ptr<Song>> v2;
remove_copy_if(v.begin(), v.end(), back_inserter(v2), [] (shared_ptr<Song> s) 
{
    return s->artist.compare(L"Bob Dylan") == 0;
});

for (const auto& s : v2)
{
    wcout << s->artist << L":" << s->title << endl;
}

Пример 4

Для приведения указателя dynamic_pointer_cast можно использовать функции static_pointer_cast, const_pointer_cast и shared_ptr. Они похожи на операторы dynamic_cast, static_cast и const_cast. В следующем примере показано, как протестировать производный тип каждого элемента в векторе базовых shared_ptr классов, а затем копировать элементы и отображать сведения о них.

vector<shared_ptr<MediaAsset>> assets {
  make_shared<Song>(L"Himesh Reshammiya", L"Tera Surroor"),
  make_shared<Song>(L"Penaz Masani", L"Tu Dil De De"),
  make_shared<Photo>(L"2011-04-06", L"Redmond, WA", L"Soccer field at Microsoft.")
};

vector<shared_ptr<MediaAsset>> photos;

copy_if(assets.begin(), assets.end(), back_inserter(photos), [] (shared_ptr<MediaAsset> p) -> bool
{
    // Use dynamic_pointer_cast to test whether
    // element is a shared_ptr<Photo>.
    shared_ptr<Photo> temp = dynamic_pointer_cast<Photo>(p);
    return temp.get() != nullptr;
});

for (const auto&  p : photos)
{
    // We know that the photos vector contains only 
    // shared_ptr<Photo> objects, so use static_cast.
    wcout << "Photo location: " << (static_pointer_cast<Photo>(p))->location << endl;
}

Пример 5

Указатель shared_ptr можно передать в другую функцию описанными ниже способами.

  • Передача shared_ptr по значению. При этом вызывается конструктор копий, увеличивается счетчик ссылок, и вызываемый объект становится владельцем. С этой операцией связаны некоторые накладные расходы, которые зависят от количества передаваемых объектов shared_ptr. Используйте этот вариант, если в соответствии с неявным или явным контрактом кода между вызывающим и вызываемым объектами последний должен быть владельцем.

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

  • Передача базового указателя или ссылки на базовый объект. Это позволяет вызываемому использовать объект, но он не разделяет право собственности на объект с вызывающим shared_ptr. Остерегайтесь ситуации, когда функция-вызова создаёт shared_ptr из переданного необработанного указателя, потому что shared_ptr у функции-вызова имеет независимое число ссылок от shared_ptrу вызывающей стороны. Когда shared_ptr в вызываемом выходит из области видимости, оно удалит объект, оставляя указатель в "shared_ptr" вызывающего, который указывает на освобожденную память. Когда вызывающий shared_ptr затем выходит из области, двойные результаты будут бесплатными. Этот параметр используется только в том случае, если контракт между вызывающим и вызываемым явно указывает, что вызывающий сохраняет владение временем существования shared_ptr.

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

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

  • Иногда, например в std::vector<shared_ptr<T>>, может быть необходимо передать каждый указатель shared_ptr в тело лямбда-выражения или в именованный объект функции. Если в лямбда-выражении или функции указатель не сохраняется, передайте shared_ptr по ссылке, чтобы не вызывать конструктор копий для каждого элемента.

void use_shared_ptr_by_value(shared_ptr<int> sp);

void use_shared_ptr_by_reference(shared_ptr<int>& sp);
void use_shared_ptr_by_const_reference(const shared_ptr<int>& sp);

void use_raw_pointer(int* p);
void use_reference(int& r);

void test() {
    auto sp = make_shared<int>(5);

    // Pass the shared_ptr by value.
    // This invokes the copy constructor, increments the reference count, and makes the callee an owner.
    use_shared_ptr_by_value(sp);

    // Pass the shared_ptr by reference or const reference.
    // In this case, the reference count isn't incremented.
    use_shared_ptr_by_reference(sp);
    use_shared_ptr_by_const_reference(sp);

    // Pass the underlying pointer or a reference to the underlying object.
    use_raw_pointer(sp.get());
    use_reference(*sp);

    // Pass the shared_ptr by value.
    // This invokes the move constructor, which doesn't increment the reference count
    // but in fact transfers ownership to the callee.
    use_shared_ptr_by_value(move(sp));
}

Пример 6

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

// Initialize two separate raw pointers.
// Note that they contain the same values.
auto song1 = new Song(L"Village People", L"YMCA");
auto song2 = new Song(L"Village People", L"YMCA");

// Create two unrelated shared_ptrs.
shared_ptr<Song> p1(song1);    
shared_ptr<Song> p2(song2);

// Unrelated shared_ptrs are never equal.
wcout << "p1 < p2 = " << std::boolalpha << (p1 < p2) << endl;
wcout << "p1 == p2 = " << std::boolalpha <<(p1 == p2) << endl;

// Related shared_ptr instances are always equal.
shared_ptr<Song> p3(p2);
wcout << "p3 == p2 = " << std::boolalpha << (p3 == p2) << endl; 

См. также

Интеллектуальные указатели (современный C++)