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

У меня проблемы с пониманием использования интеллектуальных указателей в качестве членов класса в C ++ 11. Я много читал об интеллектуальных указателях и думаю, что понимаю, как работают unique_ptr и _2 _ / _ 3_. Чего я не понимаю, так это реального использования. Кажется, что все рекомендуют использовать 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...
}

Это путь? Большое спасибо!


person michaelk    schedule 26.03.2013    source источник
comment
Это помогает четко понимать время жизни, владение и возможные значения NULL. Например, передав device конструктору settings, хотите ли вы по-прежнему иметь возможность ссылаться на него в вызывающей области или только через settings? Если последнее, unique_ptr полезно. Кроме того, у вас есть сценарий, в котором возвращаемое значение getDevice() равно null. Если нет, просто верните ссылку.   -  person Keith    schedule 27.03.2013
comment
Да, shared_ptr правильно в 8/10 случаях. Остальные 2/10 делятся между unique_ptr и weak_ptr. Кроме того, weak_ptr обычно используется для разрыва циклических ссылок; Я не уверен, что ваше использование будет считаться правильным.   -  person Collin Dauphinee    schedule 27.03.2013
comment
Прежде всего, какое право собственности вы хотите получить для члена данных device? Сначала вы должны это решить.   -  person juanchopanza    schedule 27.03.2013
comment
Хорошо, я понимаю, что как вызывающий я мог бы использовать вместо этого unique_ptr и передать право собственности при вызове конструктора, если я знаю, что он мне больше не понадобится. Но как разработчик класса Settings я не знаю, хочет ли вызывающий также сохранить ссылку. Возможно, устройство будет использоваться во многих местах. Хорошо, может быть, это именно твоя точка зрения. В этом случае я бы не был единственным владельцем, и, думаю, тогда я бы использовал shared_ptr. И: значит, умные точки заменяют указатели, но не ссылки, верно?   -  person michaelk    schedule 27.03.2013
comment
this- ›device = device; Также используйте списки инициализации.   -  person Nils    schedule 27.03.2013
comment
Как я написал в принятом ответе, есть много подразумеваемых ограничений, которые вы принимаете, делая unique_ptr переменной-членом, вероятно, главным из них является неявное удаление стандартного конструктора копирования. Это удаление, возможно, делает использование вашего класса крайне неинтуитивным для любого программиста, работающего ниже по течению. См. https://stackoverflow.com/questions/16030081/copy-constructor-for-a-class-with-unique-ptr   -  person ldog    schedule 18.04.2020


Ответы (2)


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.

person Andy Prowl    schedule 26.03.2013
comment
Спасибо большое, размышления о времени разрушения были именно тем, что помогло мне понять различия в использовании. Также было очень полезно примечание о необработанном указателе. Если я правильно понимаю, здесь тоже можно было бы weak_ptr (поскольку оба наблюдают указатели), но не обязательно? - person michaelk; 27.03.2013
comment
@LKK: Да, правильно. weak_ptr всегда является альтернативой необработанным указателям наблюдения. В некотором смысле это безопаснее, потому что вы можете проверить, не болтается ли он, прежде чем разыменовать его, но это также связано с некоторыми накладными расходами. Если вы можете легко гарантировать, что вы не собираетесь разыменовать висящий указатель, тогда у вас должно быть все в порядке с наблюдением за необработанными указателями. - person Andy Prowl; 27.03.2013
comment
@LKK, вероятно, самый простой способ убедиться, что вы не разыменовываете оборванный указатель, - это не хранить его и не передавать кому-то, кто это сделает. Конечно, вы также должны убедиться, что владелец не будет уничтожен во время текущей области видимости, но это легче увидеть, поскольку он локальный. Однако в многопоточной среде это сложнее. - person enobayram; 27.03.2013
comment
В первом случае, наверное, было бы даже лучше позволить getDevice() возвращать ссылку, не так ли? Таким образом, вызывающему абоненту не нужно будет проверять nullptr. - person vobject; 27.03.2013
comment
@AndyProwl Что касается первого случая, просто заставьте getDevice () вернуть по ссылке. AFAIU, настройки гарантируют, что устройство должно быть подключено, это не обязательно. Таким образом, использование указателей не требуется. - person mloskot; 27.03.2013
comment
@vobject: в этом случае он может вернуть ссылку, да или оболочку ссылки. Однако это не может быть обобщено на случай, когда возвращаемый указатель может быть нулевым, поэтому я подумал, что просто поместил туда указатель (моя основная цель в этом ответе заключалась в том, чтобы объяснить значение наблюдения за указателями и владения указателями в обобщенном контексте, и указать, что необработанные указатели не так уж и плохи, когда необходимы наблюдающие указатели). - person Andy Prowl; 27.03.2013
comment
@vobject: Кроме того, возврат ссылки позволит пользователю ошибочно написать что-то вроде: Device myDevice = settings.getDevice();, когда они на самом деле имели в виду Device& myDevice = settings.getDevice(); (забывая &), чего не произойдет при возврате указателя. Я также считаю, что проверка на nullptr здесь не нужна, потому что функция всегда гарантированно возвращает действительный указатель. - person Andy Prowl; 27.03.2013
comment
@AndyProwl и использование auto myDevice = settings.getDevice() потенциально позволяет избежать ошибки забывания &. - person pepper_chico; 27.03.2013
comment
@chico: Не понимаю, что вы имеете в виду. auto myDevice = settings.getDevice() создаст новый экземпляр типа Device с именем myDevice и скопирует-построит его из экземпляра, на который ссылается ссылка, возвращаемая getDevice(). Если вы хотите, чтобы myDevice был ссылкой, вам нужно сделать auto& myDevice = settings.getDevice(). Так что, если я чего-то не упускаю, мы снова в той же ситуации, что и без использования auto. - person Andy Prowl; 27.03.2013
comment
@AndyProwl true, нужно пересмотреть auto (и все универсальные ссылки). Я ожидал, что auto будет преобразован в ссылочный тип для возвращаемой ссылки, моя ошибка. - person pepper_chico; 28.03.2013
comment
@AndyProwl В этом примере кажется вероятным, что Device в любом случае не должен быть копируемым (частный конструктор копирования), что позволит избежать проблемы с пользователями, случайно написавшими Devide myDevice = settings.getDevice(). - person Edward Loper; 28.03.2013
comment
Возврат наблюдающего необработанного указателя подразумевает выполнение нескольких обязанностей. Семантика указателя подразумевает, что nullptr является возможным и действительным результатом. nullptr не нужно проверять только посредством соглашения или явной документации. Я думаю, что ссылка (const) должна быть выбором по умолчанию, с указателем наблюдения в качестве альтернативы, если вы также хотите указать, что тип является необязательным. - person Bret Kuhns; 29.03.2013
comment
Следите за std::optional на предмет возможного C ++ 14 или проверяйте boost::optional, чтобы полностью выразить правильную семантику. Кроме того, поиск в Интернете для exempt_ptr должен вернуть предложение ISO C ++ для глупого интеллектуального указателя, единственная цель которого - выразить семантику наблюдаемого указателя. - person Bret Kuhns; 29.03.2013
comment
@BretKuhns: Как я уже упоминал в предыдущем комментарии, верно, что в этом случае мы могли бы вернуть ссылку, но моя цель в ответе заключалась в том, чтобы просто предоставить простое решение, которое 1) можно было бы обобщить (применяется также, когда разрешенный указатель мог быть null), и 2) показал, что необработанные указатели плохи только при использовании для ручного управления памятью. Но я согласен с тем, что в этом конкретном случае мы могли / должны вернуть ссылку. - person Andy Prowl; 29.03.2013
comment
@AndyProwl Я видел предыдущие комментарии, но хотел подчеркнуть, что возвращение указателя наблюдения в этом случае вводит в заблуждение и не передает правильную информацию вызывающему. Раньше комментарии были более буквальными, тогда как я говорю об API и удобстве сопровождения кода. - person Bret Kuhns; 29.03.2013
comment
У меня вопрос: почему бы не вернуть std::unique_ptr<T>& вместо T*? - person user703016; 30.01.2014
comment
@Purrformance: поскольку вы не хотите отдавать право собственности на объект - передача изменяемого unique_ptr клиенту открывает возможность того, что клиент перейдет от него, таким образом приобретая право собственности и оставляя вас с нулевым (уникальным) указателем. - person Andy Prowl; 30.01.2014
comment
@ Любой Спасибо, в этом есть смысл. А как насчет std::unique_ptr<T> const&? - person user703016; 30.01.2014
comment
@Purrformance: Хотя это помешало бы клиенту двигаться (если только клиент не сумасшедший ученый, увлеченный const_casts), я лично не стал бы этого делать. Он раскрывает детали реализации, то есть тот факт, что владение уникально и реализуется через unique_ptr. Я вижу вещи так: если вы хотите / должны передать / вернуть право собственности, передайте / верните интеллектуальный указатель (unique_ptr или shared_ptr, в зависимости от типа владения). Если вы не хотите / не хотите передавать / возвращать право собственности, используйте (правильно квалифицированный const) указатель или ссылку, в основном в зависимости от того, может ли аргумент быть нулевым или нет. - person Andy Prowl; 30.01.2014
comment
@AndyProwl Если Settings не интересуется Device жизнью (просто использует ее), правильно ли иметь необработанный указатель в качестве поля класса или в любом случае лучше использовать умный указатель? - person Marco Stramezzi; 18.10.2017
comment
@MarcoStramezzi Да, в этом случае необработанный указатель будет правильным. - person Andy Prowl; 18.10.2017
comment
@AndyProwl: вы рекомендуете использовать необработанный указатель / ссылку в качестве возвращаемого типа getDevice (). Допустим, Настройки владеют устройством. Также допустим, что я совершил ошибку, неправильно используя getDevice (), сохранив возвращенный необработанный указатель / ссылку дольше, чем живет объект Settings. Мне было бы сложно отладить неопределенное поведение. Могу ли я использовать интеллектуальные указатели, чтобы предотвратить это или, по крайней мере, упростить отладку? Я представляю, как использовать shared_ptr как член и как возвращаемое значение. Мне нужно было бы выбросить исключение при уничтожении, если член shared_ptr не содержит последнюю ссылку. Возможно ли это? - person Silicomancer; 07.03.2020
comment
Это очень неполный ответ. Например, здесь не упоминаются все неявные ограничения, которые вы принимаете, выбирая unique_ptr вместо shared_ptr, вероятно, главным из них является неявное удаление стандартного конструктора копирования, см. [1]. Пожалуйста, внимательно изучите тему, прежде чем публиковать неполный ответ. [1] stackoverflow .com / questions / 16030081 / - person ldog; 18.04.2020

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_ptr s и 1 для все указатели (shared_ptr и weak_ptr). Когда все shared_ptr удалены, указатель удаляется. Когда требуется указатель от weak_ptr, следует использовать lock для получения указателя, если он существует.

person Naszta    schedule 26.03.2013
comment
Итак, если я правильно понимаю ваш ответ, умные указатели заменяют необработанные указатели, но не обязательно ссылки? - person michaelk; 27.03.2013
comment
Есть ли на самом деле два счетчика ссылок в shared_ptr? Не могли бы вы объяснить почему? Насколько я понимаю, weak_ptr не нужно подсчитывать, потому что он просто создает новый shared_ptr при работе с объектом (если базовый объект все еще существует). - person Björn Pollex; 27.03.2013
comment
@ BjörnPollex: Я создал для вас короткий пример: ссылка. Я не реализовал все, только конструкторы копирования и lock. Версия boost также потокобезопасна при подсчете ссылок (delete вызывается только один раз). - person Naszta; 28.03.2013
comment
@Naszta: Ваш пример показывает, что это возможно реализовать с использованием двух счетчиков ссылок, но ваш ответ предполагает, что это обязательно, чего я не думаю. Не могли бы вы пояснить это в своем ответе? - person Björn Pollex; 28.03.2013
comment
@ BjörnPollex: Послушайте, вот как работает реализация Boost. В Boost атомарные счетчики используются для обеспечения безопасности потоков. Это все. Если вы меня не поверите, вы можете легко проверить это в реализации Boost (или в VS2012 самостоятельно). - person Naszta; 28.03.2013
comment
@ BjörnPollex, чтобы weak_ptr::lock() мог определить, истек ли срок действия объекта, он должен проверить блок управления, который содержит первый счетчик ссылок и указатель на объект, поэтому блок управления не должен быть уничтожен, пока есть какие-либо weak_ptr объекты, все еще используемые, поэтому необходимо отслеживать количество weak_ptr объектов, что и делает второй счетчик ссылок. Объект уничтожается, когда первый счетчик ссылок падает до нуля, блок управления уничтожается, когда второй счетчик ссылок падает до нуля. - person Jonathan Wakely; 31.03.2013