Использование умных указателей для учеников

159

У меня проблемы с пониманием использования умных указателей в качестве членов класса в C ++ 11. Я много читал об умных указателях и думаю, что понимаю, как unique_ptrи shared_ptr/ или weak_ptrработаю в целом. То, что я не понимаю, является реальным использованием. Кажется, что все рекомендуют использовать unique_ptrкак способ идти почти все время. Но как бы я реализовать что-то вроде этого:

class Device {
};

class Settings {
    Device *device;
public:
    Settings(Device *device) {
        this->device = device;
    }

    Device *getDevice() {
        return device;
    }
};    

int main() {
    Device *device = new Device();
    Settings settings(device);
    // ...
    Device *myDevice = settings.getDevice();
    // do something with myDevice...
}

Допустим, я хотел бы заменить указатели умными указателями. А unique_ptrне будет работать из-за getDevice(), верно? Так что это время, когда я использую shared_ptrи weak_ptr? Нет способа использования unique_ptr? Мне кажется, что в большинстве случаев shared_ptrимеет больше смысла, если я не использую указатель в очень маленькой области видимости?

class Device {
};

class Settings {
    std::shared_ptr<Device> device;
public:
    Settings(std::shared_ptr<Device> device) {
        this->device = device;
    }

    std::weak_ptr<Device> getDevice() {
        return device;
    }
};

int main() {
    std::shared_ptr<Device> device(new Device());
    Settings settings(device);
    // ...
    std::weak_ptr<Device> myDevice = settings.getDevice();
    // do something with myDevice...
}

Это путь? Огромное спасибо!

michaelk
источник
4
Это помогает быть действительно ясным относительно срока службы, владения и возможных нулей. Например, перейдя deviceк конструктору settings, хотите ли вы по-прежнему иметь возможность ссылаться на него в области вызова или только через settings? Если последнее, unique_ptrполезно. Кроме того , у вас есть сценарий , в котором возвращаемое значение getDevice()является null. Если нет, просто верните ссылку.
Кит
2
Да, shared_ptrправильно в 8/10 случаях. Остальные 2/10 разделены между unique_ptrи weak_ptr. Кроме того, weak_ptrобычно используется для разрыва циклических ссылок; Я не уверен, что ваше использование будет считаться правильным.
Коллин Дауфини
2
Прежде всего, какую собственность вы хотите получить для deviceчлена данных? Сначала вы должны решить это.
juanchopanza
1
Хорошо, я понимаю, что в качестве вызывающей стороны я мог бы использовать unique_ptrвместо этого и отказаться от владения при вызове конструктора, если я знаю, что он мне больше не нужен. Но, как дизайнер Settingsкласса, я не знаю, хочет ли звонящий сохранить ссылку. Возможно, устройство будет использоваться во многих местах. Хорошо, возможно, это именно твоя точка зрения. В этом случае я не был бы единственным владельцем, и я думаю, что именно тогда я бы использовал shared_ptr. И так: умные точки заменяют указатели, а не ссылки, верно?
Михаил
это-> устройство = устройство; Также используйте списки инициализации.
Нильс

Ответы:

202

А unique_ptrне будет работать из-за getDevice(), верно?

Нет, не обязательно Здесь важно определить подходящую политику владения для вашего Deviceобъекта, т.е. кто будет владельцем объекта, на который указывает ваш (умный) указатель.

Это собирается быть экземпляр Settingsобъекта в одиночку ? Придется Deviceли уничтожать Settingsобъект автоматически при его разрушении или он переживет этот объект?

В первом случае std::unique_ptrэто то , что вам нужно, так как он делает Settingsединственным (уникальным) владельцем указанного объекта и единственным объектом, который отвечает за его уничтожение.

В соответствии с этим предположением getDevice()следует возвращать простой указатель наблюдения (указатели наблюдения - это указатели, которые не поддерживают указанный объект в живых). Самым простым видом указателя наблюдения является необработанный указатель:

#include <memory>

class Device {
};

class Settings {
    std::unique_ptr<Device> device;
public:
    Settings(std::unique_ptr<Device> d) {
        device = std::move(d);
    }

    Device* getDevice() {
        return device.get();
    }
};

int main() {
    std::unique_ptr<Device> device(new Device());
    Settings settings(std::move(device));
    // ...
    Device *myDevice = settings.getDevice();
    // do something with myDevice...
}

[ ПРИМЕЧАНИЕ 1: Вам может быть интересно, почему я здесь использую сырые указатели, когда все говорят, что необработанные указатели плохие, небезопасные и опасные. На самом деле, это ценное предупреждение, но важно поместить его в правильный контекст: необработанные указатели плохи, когда используются для ручного управления памятью , то есть для выделения и освобождения объектов через newи delete. При использовании исключительно в качестве средства для достижения ссылочной семантики и обхода несобственников при наблюдении указателей в исходных указателях нет ничего опасного, за исключением, возможно, того факта, что следует избегать разыменования висящего указателя. - КОНЕЦ ПРИМЕЧАНИЕ 1 ]

[ ПРИМЕЧАНИЕ 2: Как выяснилось в комментариях, в этом конкретном случае, когда право собственности является уникальным и всегда гарантированно присутствует принадлежащий объект (т. Е. Внутренний элемент данных deviceникогда не будет nullptr), функция getDevice()может (и, возможно, должна) вернуть ссылку, а не указатель. Хотя это и правда, я решил вернуть здесь необработанный указатель, потому что я имел в виду, что это будет краткий ответ, который можно обобщить на случай, где это deviceвозможно nullptr, и показать, что необработанные указатели в порядке, если их не использовать для ручное управление памятью. - КОНЕЦ ПРИМЕЧАНИЕ 2 ]


Конечно, ситуация радикально отличается, если ваш Settingsобъект не должен иметь исключительное право собственности на устройство. Это может иметь место, например, если разрушение Settingsобъекта не должно также подразумевать разрушение заостренного Deviceобъекта.

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

Чтобы помочь вам понять это, вы можете спросить себя, есть ли какие-либо другие объекты, кроме тех, Settingsкоторые имеют право поддерживать Deviceобъект в течение всего времени, пока он удерживает указатель на него, вместо того, чтобы быть просто пассивными наблюдателями. Если это действительно так, то вам нужна политика совместного владения , которая std::shared_ptrпредлагает следующее:

#include <memory>

class Device {
};

class Settings {
    std::shared_ptr<Device> device;
public:
    Settings(std::shared_ptr<Device> const& d) {
        device = d;
    }

    std::shared_ptr<Device> getDevice() {
        return device;
    }
};

int main() {
    std::shared_ptr<Device> device = std::make_shared<Device>();
    Settings settings(device);
    // ...
    std::shared_ptr<Device> myDevice = settings.getDevice();
    // do something with myDevice...
}

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

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

Энди Проул
источник
4
@LKK: Да, правильно. A weak_ptrвсегда является альтернативой необработанным указателям наблюдения. В некотором смысле это безопаснее, потому что вы можете проверить, не свисает ли оно, прежде чем разыменовывать его, но оно также связано с некоторыми накладными расходами. Если вы можете легко гарантировать, что вы не собираетесь разыменовывать висящий указатель, то вы должны хорошо соблюдать необработанные указатели
Andy Prowl
6
В первом случае, возможно, было бы даже лучше позволить getDevice()вернуть ссылку, не так ли? Таким образом, абонент не должен был бы проверить nullptr.
vobject
5
@chico: Не уверен, что вы имеете в виду. auto myDevice = settings.getDevice()создаст новый экземпляр Deviceвызываемого типа myDeviceи создаст его копию из того, на который ссылается getDevice()возвращаемая ссылка . Если вы хотите myDeviceбыть ссылкой, вам нужно сделать auto& myDevice = settings.getDevice(). Так что, если я что-то упустил, мы вернулись в той же ситуации, что и без использования auto.
Энди Prowl
2
@Purrformance: поскольку вы не хотите отдавать право собственности на объект - передача изменяемого объекта unique_ptrклиенту открывает возможность того, что клиент от него уйдет, приобретая таким образом право собственности и оставляя вас с нулевым (уникальным) указателем.
Энди Prowl
7
@Purrformance: хотя это помешало бы клиенту двигаться (если клиент не безумный ученый, увлеченный const_castею), я лично не стал бы этого делать. Он раскрывает детали реализации, то есть тот факт, что право собственности является уникальным и реализуется через unique_ptr. Я вижу вещи таким образом: если вы хотите / должны передать / вернуть право собственности, передайте / верните умный указатель ( unique_ptrили shared_ptr, в зависимости от вида собственности). Если вы не хотите / не должны передавать / возвращать владельца, используйте constуказатель или ссылку (правильно определенную), в основном в зависимости от того, может ли аргумент быть нулевым или нет.
Энди Проул
0
class Device {
};

class Settings {
    std::shared_ptr<Device> device;
public:
    Settings(const std::shared_ptr<Device>& device) : device(device) {

    }

    const std::shared_ptr<Device>& getDevice() {
        return device;
    }
};

int main()
{
    std::shared_ptr<Device> device(new Device());
    Settings settings(device);
    // ...
    std::shared_ptr<Device> myDevice(settings.getDevice());
    // do something with myDevice...
    return 0;
}

week_ptrиспользуется только для эталонных циклов. Граф зависимостей должен быть ациклически ориентированным графом. В общих указателях есть 2 счетчика ссылок: 1 для shared_ptrs и 1 для всех указателей ( shared_ptrи weak_ptr). Когда все shared_ptrs удалены, указатель удаляется. Когда указатель необходим от weak_ptr, lockдолжен использоваться для получения указателя, если он существует.

Naszta
источник
Так что, если я правильно понимаю ваш ответ, умные указатели заменяют необработанные указатели, но не обязательно ссылки?
Майкл
Есть ли на самом деле два подсчета ссылок в shared_ptr? Не могли бы вы объяснить, почему? Насколько я понимаю, weak_ptrне нужно считать, потому что он просто создает новое shared_ptrпри работе с объектом (если базовый объект все еще существует).
Бьорн Поллекс
@ BjörnPollex: я создал для вас короткий пример: ссылка . Я не реализовал все, только конструкторы копирования и lock. Надежная версия также является поточно-ориентированной при подсчете ссылок ( deleteвызывается только один раз).
Наста
@Naszta: Ваш пример показывает, что это можно реализовать, используя два подсчета ссылок, но ваш ответ предполагает, что это необходимо , но я не верю, что это так. Не могли бы вы уточнить это в своем ответе?
Бьорн Поллекс
1
@ BjörnPollex, weak_ptr::lock()чтобы определить, истек ли срок действия объекта, он должен проверить «блок управления», содержащий первый счетчик ссылок и указатель на объект, поэтому блок управления не должен быть уничтожен, пока все weak_ptrеще используются какие-либо объекты, таким образом, число weak_ptrобъектов должно отслеживаться, что и делает второй счетчик ссылок. Объект разрушается, когда число первых ссылок падает до нуля, блок управления разрушается, когда количество вторых ссылок падает до нуля.
Джонатан Уэйкли