Когда уместно назначать unique_ptr дляshared_ptr?

Это продолжение вопроса, который был задан здесь: Могут ли C++11 unique_ptr иshared_ptr преобразовываться в тип друг друга? и точнее ответ, получивший наибольшее количество голосов.

Мое внимание привлек вот этот фрагмент:

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

А потом я увидел повторяющийся вопрос разрешено ли присвоение unique_ptr значениюshared_ptr? у которого был комментарий:

Почему не должно быть? Это даже безопаснее, чем присваивать обычный указатель... [подробнее здесь].

Однако пользователь не уточнил, почему это безопаснее, чем назначение обычного указателя. Мне интересно, в каких случаях было бы лучше назначить unique_ptr дляshared_ptr? Если я правильно понял первый цитируемый раздел, лучше всего использовать такие случаи: (Предположим, что у нас есть рабочий базовый класс Car и все CarType наследуются от Car.)

enum class CarType
{
    Sedan,
    SUV,
    Crossover,
    Truck,
    Bus
};

class MyRental
{
  public:
    MyRental();

    void assignRentalCar(CarType type);
    void doDriveAction();
    void doTurnAction();
    void doParkAction();

  private:
    std::shared_ptr<Car> currentCar;
    std::shared_ptr<Bus> bus; // a Oversized Vehicle Bus : (public OversizedCar : public Car)

};

Предположим, что выше в наших частных членах у нас есть особый случай, когда переменная-член bus наследует от класса OversizeCar, который наследует от класса Car.

MyRental::MyRental() : currentCar(std::make_unique<Sedan>()),
                       bus(std::make_unique<Bus>())
{}
                         

void MyRental::assignRentalCar(CarType type)
{
  switch(type)
  {
    case CarType::SUV: currentCar = std::make_unique<SUV>(); break;
    case CarType::Crossover: currentCar = std::make_unique<Crossover>(); break;
    case CarType::Truck: currentCar = std::make_unique<Truck>(); break;
    case CarType::Bus: currentCar = bus; break; // allowed because currentCar is a shared_ptr
    case CarType::Sedan:
    case default: currentCar = std::make_unique<Sedan>(); break;
  }
}

void MyRental::doDriveAction()
{
  currentCar->handleDrive();
}

void MyRental::doTurnAction()
{
  currentCar->handleTurn();
}

void MyRental::doParkAction()
{
  currentCar->handlePark();
}

Уместно ли в этом случае присвоить unique_ptrshared_ptr? И если да, то почему это уместно? Если это неуместно и лучше вместо этого присвоить shared_ptr, то когда лучше всего присвоить unique_ptrshared_ptr?

TLDR: Никогда. Общий указатель моделирует совместное владение, а уникальный указатель – уникальное владение. Это две разные концепции (преобразование даже не должно было поддерживаться C++)

Pepijn Kramer 26.04.2024 21:07

Это «безопаснее», потому что политика владения очень ясна. Принимая во внимание, Foo* p = q; владеет ли p Фу сейчас и несет ли ответственность за его уничтожение? Или q все еще владеет Фу? Foo* p = std::exchange(q, nullptr); было бы более понятно, но большая часть кода, использующего необработанные указатели, просто выполняет Foo* p = q; образом, и оставляет выяснение принадлежности в качестве упражнения для читателя.

Eljay 26.04.2024 21:07

У меня здесь два разных ответа, ха-ха.

Sailanarmo 26.04.2024 21:08

Мой ответ был общим случаем. Я вижу случай, когда фабрика возвращает вам unique_pointer (никогда необработанный указатель). И затем вы решаете поделиться этим правом собственности с другими частями кода, но я все равно смоделировал бы это по-другому (например, переместил уникальный указатель на другой класс, которым вы затем можете поделиться).

Pepijn Kramer 26.04.2024 21:10

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

Pepijn Kramer 26.04.2024 21:11

«Возвращая std::unique_ptr, фабрики предоставляют вызывающим абонентам наиболее эффективный интеллектуальный указатель, но не мешают вызывающим абонентам заменить его более гибким аналогом». - Если вызывающий абонент хочет совместного владения, создание std::shared_ptr через std::make_shared() более эффективно, чем создание std::shared_ptr из std::unique_ptr. Использование std::make_shared() требует только 1 выделения памяти, тогда как перенос std::unique_ptr в std::shared_ptr предполагает 2 выделения памяти.

Remy Lebeau 26.04.2024 22:19

Предположим, я владею чем-то, а затем говорю: «Я откажусь от владения этой вещью, пока она будет принадлежать всем, кто того пожелает». Я знаю, что есть те, кто скорее умрет, чем подчинится такому коммунизму, но действительно ли вы считаете это небезопасным? Действительно, это «безопаснее», чем отдавать право собственности на то, в чем я не могу быть абсолютно уверен, что это мое.

molbdnilo 26.04.2024 22:25

«Однако пользователь не уточнил, почему это безопаснее, чем назначение обычного указателя». Вероятно, потому, что (это был просто комментарий) причины соответствуют причинам, по которым std::unique_ptr безопаснее, чем владение необработанным указателем. То есть причины мало (если вообще имеют) отношения к std::shared_ptr.

JaMiT 26.04.2024 22:50

Ваш код не очень хорош (или является хорошей иллюстрацией для изучения ваших собственных вопросов об уникальных и общих точках), потому что фабричная функция не учитывается для использования независимо от класса MyRental. Как правило, вы хотите, чтобы фабрику мог использовать любой, кто интересуется объектами Car, даже если они не используют объекты MyRental. Учебный подход заключается в том, чтобы автономная или static-членная фабричная функция возвращала std::unique_ptr<Car>, поэтому те, кто хочет уникальное владение, уже имеют его, а те, кто хочет совместное использование, могут выполнить преобразование без дополнительных затрат, чем из необработанного указателя.

Tony Delroy 04.05.2024 04:27

Однако, как отмечает Реми, общие указатели, как известно, более эффективно создаются с помощью make_shared<T>(...), поскольку он может выполнить одно выделение памяти для блока управления и первого общего указателя. По этой причине, если вы действительно заботитесь о поддержке оптимальной производительности, вы можете иметь отдельные фабричные функции — в идеале совместно использовать реализацию и использовать аргумент шаблона для определения типа возвращаемого значения и используемой функции.

Tony Delroy 04.05.2024 04:31
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
10
75
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Мне интересно, в каких случаях было бы лучше назначить unique_ptr дляshared_ptr?

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

  1. У вас есть значение u типа U, которое нужно назначить.
  2. У вас есть объект s типа S, которому нужно назначить.
  3. Присвоение s = u синтаксически допустимо и полезно для вашего кода.
  4. Строительство S s(u) не является разумным вариантом. Если вы можете разумно использовать конструкцию вместо присваивания, сделайте это.

Эти критерии хороши для многих типов, в том числе когда U есть std::unique_ptr&& и S есть std::shared_ptr. Единственное предостережение в случае интеллектуального указателя заключается в том, что присваивание должно происходить из значения r. То есть, если unique_ptr не возвращается функцией, вам, вероятно, придется переместить его, как в случае с s = std::move(u). Обратите внимание, что это назначение перемещения приведет к тому, что u будет нулевым; у вас не будет s и u одновременно указывать на один и тот же объект.

По сути, используйте присваивание, когда оно естественно, и избегайте его, когда это не так.


Однако вы спросили о «лучшем», а не о «хорошем». Это приводит к еще одному соображении, также не касающемуся интеллектуальных указателей: избегайте ненужных преобразований. Присвоение unique_ptr<Truck>shared_ptr<Car> предполагает более сложное преобразование, чем преобразование shared_ptr<Truck> в shared_ptr<Car>. В вашем примере вы можете легко упростить, заменив std::make_unique на std::make_shared. Будьте проще и не заставляйте других спрашивать: «конвертируется ли unique_ptr в общий_ptr?»

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

Другие вопросы по теме