Новые и удалить все еще полезны в С++ 14?

Учитывая наличие make_unique и make_shared, а также автоматическое удаление деструкторами unique_ptr и shared_ptr, каковы ситуации (помимо поддержки устаревшего кода) для использования new и delete в С++ 14?

Ответ 1

В то время как интеллектуальные указатели предпочтительнее для сырых указателей во многих случаях, в С++ 14 есть еще много прецедентов для new/delete.

Если вам нужно написать что-нибудь, что требует создания на месте, например:

  • пул памяти
  • распределитель
  • отмеченный вариант
  • двоичные сообщения в буфер

вам нужно будет использовать размещение new и, возможно, delete. Ничего подобного.

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

Даже для стандартных интеллектуальных указателей вам все равно потребуется new, если вы хотите использовать пользовательские удалители, поскольку make_unique и make_shared не позволяют этого.

Ответ 2

Общепринято использовать make_unique и make_shared вместо необработанных вызовов new. Однако это не обязательно. Предполагая, что вы решили следовать этому соглашению, есть несколько мест для использования new.

Во-первых, нестандартное размещение new (я буду пренебрегать "нестандартной" частью и просто назвать ее местом размещения new) - это совершенно другая карточная игра, чем стандартная (без размещения) new, Он логически сопряжен с ручным вызовом деструктора. Стандарт new как приобретает ресурс из свободного хранилища, так и создает в нем объект. Он сопряжен с delete, который уничтожает объект и перерабатывает хранилище в свободное хранилище. В некотором смысле, стандартный new вызывает внутреннее размещение new, а стандартный delete вызывает деструктор внутри.

Размещение new - это то, как вы напрямую вызываете конструктор на каком-либо хранилище, и требуется для расширенного кода управления жизненным циклом. Если вы выполняете optional, безопасный тип union на выровненном хранилище или интеллектуальный указатель (с унифицированным хранилищем и не унифицированным временем жизни, например make_shared), вы будете использовать размещение new. Затем в конце определенного времени жизни объекта вы вызываете его деструктор. Подобно не размещению new и delete, размещение new и ручные вызовы деструктора попадают парами.

Пользовательское размещение new является еще одной причиной использования new. Пользовательское размещение new может использоваться для выделения ресурсов из неглобального распределения пула - распределения по выделенной области или распределения на общую страницу разделяемой памяти, распределения в общую память видеокарты и т.д. - и в других целях. Если вы хотите написать make_unique_from_custom, который выделяет свою память с помощью специального места размещения new, вам нужно будет использовать ключевое слово new. Пользовательское размещение new может действовать как новое размещение (тем, что оно фактически не приобретает ресурсы, а скорее как ресурс каким-то образом передается), или он может действовать как стандартный new (тем, что он приобретает ресурсы, возможно, используя аргументы переданы).

Пользовательское размещение delete вызывается, если выбрано произвольное размещение new, поэтому вам может потребоваться его написать. В С++ вы не называете пользовательское размещение delete, оно (С++) вызывает вас (r overload).

Наконец, make_shared и make_unique являются неполными функциями, поскольку они не поддерживают пользовательские удалители.

Если вы пишете make_unique_with_deleter, вы все равно можете использовать make_unique для выделения данных, а .release() - для вашего уникального обращения с делом. Если ваш дебит хочет записать свое состояние в буфер с указателем, а не в unique_ptr или в отдельное выделение, вам нужно будет использовать размещение new здесь.

Для make_shared клиентский код не имеет доступа к коду создания "подсчета ссылок". Насколько я могу судить, вы не можете легко иметь "комбинированное выделение объекта и счетного блока подсчета" и пользовательский отладчик.

Кроме того, make_shared заставляет распределение ресурсов (хранилище) для самого объекта сохраняться до тех пор, пока weak_ptr сохраняется: в некоторых случаях это может быть нежелательно, поэтому вы хотите сделать shared_ptr<T>(new T(...)), чтобы избежать этого.

В тех немногих случаях, когда вы хотите вызвать не размещение new, вы можете вызвать make_unique, а затем .release() указатель, если вы хотите управлять отдельно от этого unique_ptr. Это увеличивает ваш охват RAII ресурсами и означает, что если есть исключения или другие логические ошибки, вы менее склонны к утечке.


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

template<class T, class D>
struct custom_delete {
  std::tuple<
    std::aligned_storage< sizeof(T), alignof(T) >,
    D,
    bool
  > data;
  bool bCreated() const { return std::get<2>(data); }
  void markAsCreated() { std::get<2>()=true; }
  D&& d()&& { return std::get<1>(std::move(data)); }
  void* buff() { return &std::get<0>(data); }
  T* t() { return static_cast<T*>(static_cast<void*>(buff())); }
  template<class...Ts>
  explicit custom_delete(Ts...&&ts):data(
    {},D(std::forward<Ts>(ts)...),false
  ){}
  custom_delete(custom_delete&&)=default;
  ~custom_delete() {
    if (bCreated())
      std::move(*this).d()(t());
  }
};

template<class T, class D, class...Ts, class dD=std::decay_t<D>>
std::shared_ptr<T> make_shared_with_deleter(
  D&& d,
  Ts&&... ts
) {
  auto internal = std::make_shared<custom_delete<T, dD>>(std::forward<D>(d));
  if (!internal) return {};
  T* r = new(internal->data.buff()) T(std::forward<Ts>(ts...));
  internal->markAsCreated();
  return { internal, r };
}

Я думаю, что это нужно делать. Я сделал попытку позволить безстоящим лицам удалить ненужное пространство с помощью tuple, но я, возможно, прищурился.

В решении с качеством библиотеки, если T::T(Ts...) - noexcept, я мог бы удалить служебные данные bCreated, так как не было бы возможности, чтобы custom_delete нужно было уничтожить до того, как T будет построены.

Ответ 3

Единственная причина, по которой я могу думать, это то, что иногда вы можете использовать пользовательский делетер со своими unique_ptr или shared_ptr. Чтобы использовать пользовательский удалён, вам нужно создать интеллектуальный указатель напрямую, передав результат new. Даже это не часто, но на практике это происходит.

Кроме того, кажется, что make_shared/make_unique должен охватывать почти все виды использования.

Ответ 4

Я бы сказал, что единственной причиной для new и delete является реализация других видов интеллектуальных указателей.

Например, библиотека по-прежнему не имеет навязчивых указателей в качестве boost:: intrusive_ptr, что очень жалко, так как они превосходят по соображениям производительности общие указатели, как указывает Андрей Александреску.