Подтвердить что ты не робот

Существуют ли какие-либо допустимые варианты использования новых и удаленных, необработанных указателей или массивов c-стиля с современным С++?

Здесь заметный видео (Остановить обучение C) об изменении этой парадигмы, чтобы принять участие в обучении языку С++.

А также заметное сообщение в блоге

У меня есть мечта...

Я мечтаю о так называемых курсах/классах С++/куррикулумах, которые перестанут преподавать (требуя) своих учеников:...

Так как С++ 11 в качестве стандартного стандарта, мы имеем Управление динамической памятью, а также интеллектуальные указатели. < ш > Даже из более ранних стандартов у нас есть стандартная std::string вместо c-style NUL завершенных массивов символов).

Вопрос в жирный:

Отмените переопределение места размещения new, , есть ли какой-либо допустимый прецедент, который не может быть достигнут с помощью интеллектуальных указателей или стандартных контейнеров, но только с использованием new и delete непосредственно (кроме реализация таких классов контейнера/умных указателей, конечно)?

Иногда он по слухам (например здесь или здесь), что с помощью new и delete с ручным управлением может быть "более эффективным" для определенных случаев. Каковы они на самом деле?. Разве эти краевые случаи не должны отслеживать распределения так же, как стандартные контейнеры или интеллектуальные указатели?

Почти то же самое для необработанных массивов фиксированного размера c-стиля: в настоящее время существует std::array, что позволяет легко и синтаксически выполнять все виды присвоения, копирования, ссылки и т.д., как и ожидалось всеми. Есть ли какие-либо варианты использования T myArray[N]; массива c-style в предпочтении std::array<T,N> myArray;?


Относительно взаимодействия с сторонними библиотеками:

Предположим, что сторонняя библиотека возвращает исходные указатели, выделенные с помощью new, например

MyType* LibApi::CreateNewType() {
    return new MyType(someParams);
}

вы всегда можете привязать это к умному указателю, чтобы гарантировать, что вызывается delete:

std::unique_ptr<MyType> foo = LibApi::CreateNewType();

даже если API требует, чтобы вы вызывали свою устаревшую функцию, чтобы освободить ресурс, например

void LibApi::FreeMyType(MyType* foo);

вы все равно можете предоставить функцию дебетера:

std::unique_ptr<MyType, LibApi::FreeMyType> foo = LibApi::CreateNewType();

Меня особенно интересуют действительные "повседневные" варианты использования, в отличие от требований академического/образовательного назначения и ограничений, которые не охватываются указанными стандартными средствами.
То, что new и delete может быть использовано в фреймворках управления памятью/сборщиками мусора или стандартная реализация контейнера, не может быть и речи 1.


Одна главная мотивация...

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

Они часто упоминаются как основы управления памятью, которые IMO явно ошибочно/неправильно поняты, подходящие для лекций и задач для начинающих.


1) Добавить.: Что касается этого абзаца, это должен быть ясный индикатор того, что new и delete не для начинающих С++ студентов, но его следует оставить для более продвинутых курсов.

4b9b3361

Ответ 1

Когда собственность не должна быть локальной.

В качестве примера контейнер-указатель может не захотеть владеть над указателями в нем, чтобы он находился в самих указателях. Если вы попытаетесь написать связанный список с прямыми уникальными ptrs, во время уничтожения вы можете легко взорвать стек.

A vector -подобный контейнер владеющих указателей может быть лучше подходит для хранения операции удаления на уровне контейнера или подконтейнера, а не на уровне элемента.

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

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

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

Определение правильности здесь сложно, но не невозможно. Программы, которые являются правильными и имеют такую ​​сложную семантику собственности, существуют.


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

Ответ 2

Я собираюсь быть противоположным и записываться как "нет" (по крайней мере, на вопрос, который, я уверен, вы действительно намеревались спросить, в большинстве случаев, которые были приведены).

Похоже, что очевидные варианты использования new и delete (например, необработанная память для кучи GC, хранилище для контейнера) действительно не являются. Для этих случаев вы хотите "сырое" хранилище, а не объект (или массив объектов, что означает new и new[] соответственно).

Так как вы хотите сырое хранилище, вам действительно нужно/нужно использовать operator new и operator delete для управления самим необработанным хранилищем. Затем вы используете размещение new для создания объектов в этом необработанном хранилище и непосредственно вызываете деструктор для уничтожения объектов. В зависимости от ситуации вы можете использовать для этого уровень косвенности - например, контейнеры в стандартной библиотеке используют класс Allocator для обработки этих задач. Это передается как параметр шаблона, который обеспечивает точку настройки (например, способ оптимизации распределения на основе типичного шаблона использования конкретного контейнера).

Итак, для этих ситуаций вы в конечном итоге используете ключевое слово new (как в размещении new, так и при вызове operator new), но не что-то вроде T *t = new T[N];, и это я уверен, что вы чтобы спросить о.

Ответ 3

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

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

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

Ответ 4

OP специально задает вопрос о том, как/когда перенос будет более эффективным в повседневном использовании - и я буду обращаться к нему.

Предполагая, что современный компилятор /stl/platform на сегодняшний день не используется каждый день, когда использование новых и удаленных приложений будет более эффективным. Для случая shared_ptr я считаю, что это будет незначительным. В чрезвычайно трудной петле может быть что-то получить, просто используя raw new, чтобы избежать подсчета ref (и найти какой-то другой метод очистки - если вы не навязали вам, вы решили использовать shared_ptr по какой-то причине) но это не обычный или обычный пример. Для unique_ptr на самом деле нет никакой разницы, поэтому я думаю, что можно с уверенностью сказать, что это скорее слухи и фольклор, и эта производительность не имеет никакого значения вообще (разница не будет измерима в обычных случаях).

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

Ответ 5

Некоторые API-интерфейсы могут ожидать, что вы создадите объекты с помощью new, но возьмите на себя владение объектом. Например, библиотека Qt имеет родительско-дочернюю модель, в которой родитель удаляет свои дочерние элементы. Если вы используете интеллектуальный указатель, вы будете сталкиваться с проблемами с двойным удалением, если не будете осторожны.

Пример:

{
    // parentWidget has no parent.
    QWidget parentWidget(nullptr);

    // childWidget is created with parentWidget as parent.
    auto childWidget = new QWidget(&parentWidget);
}
// At this point, parentWidget is destroyed and it deletes childWidget
// automatically.

В этом конкретном примере вы все равно можете использовать интеллектуальный указатель, и все будет хорошо:

{
    QWidget parentWidget(nullptr);
    auto childWidget = std::make_unique<QWidget>(&parentWidget);
}

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

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

Возможно, вы думаете, что для решения этой проблемы вам просто нужно избегать модели parent-child и создавать все ваши виджеты в стеке и без родителя:

QWidget childWidget(nullptr);

или с помощью умного указателя и без родителя:

auto childWidget = std::make_unique<QWidget>(nullptr);

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

Самый простой способ работать с этим - использовать new. Все остальное либо вызывает неприятности, либо больше работы, либо и то, и другое.

Такие API-интерфейсы можно найти в современном, не устаревшем программном обеспечении (например, Qt), и были разработаны много лет назад, задолго до того, как интеллектуальные указатели были предметом. Они не могут быть легко изменены, так как это сломает существующий код.

Ответ 6

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

Теперь о случаях, о которых я могу думать:

  • разработка контейнеров или других концепций низкого уровня - ведь сама стандартная библиотека написана на С++, и она использует исходные указатели, новые и удаленные
  • оптимизация низкого уровня. Это никогда не должно быть проблемой первого класса, потому что компиляторы достаточно умны, чтобы оптимизировать стандартный код, а удобство обслуживания обычно более важно, чем сырая производительность. Но когда профилирование показывает, что блок кода составляет более 80% времени выполнения, оптимизация низкого уровня имеет смысл, и это одна из причин того, что стандартная библиотека низкого уровня C по-прежнему является частью стандартов С++.

Ответ 7

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

Представьте, что вы кодируете некоторый Scheme интерпретатор в С++ 11 (или некоторый интерпретатор байт-кода Ocaml). Этот язык требует, чтобы вы закодировали GC (поэтому вам нужно ввести код в С++). Так что собственность не локальная, а ответила Yakk. И вы хотите, чтобы мусор собирал значения Scheme, а не сырую память!

Вероятно, вы будете использовать явные new и delete.

Другими словами, С++ 11 умные указатели поддерживают некоторые подсчет ссылок. Но это плохой метод GC (он не дружит с круговыми ссылками, которые распространены в Схеме).

Например, наивный способ реализации простого mark-and-sweep GC должен собирать в каком-то глобальном контейнере все указатели значений схемы и т.д.

Прочтите также Руководство GC.

Ответ 8

Когда вам нужно передать что-то через границу DLL. Вы (почти) не можете сделать это с помощью умных указателей.

Ответ 9

Иногда вам приходится вызывать new при использовании частных конструкторов.

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

Ответ 10

3 общих примера, где вам нужно использовать новый вместо make_...:

  • Если ваш объект не имеет открытого конструктора
  • Если вы хотите использовать пользовательский deleter
  • Если вы используете С++ 11 и хотите создать объект, которым управляет уникальный_ptr (хотя я бы рекомендовал написать ваш собственный make_unique в этом случае).

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

2-3 (возможно, не столь распространенные) примеры, где вы не хотите/не можете использовать интеллектуальные указатели:

  • Если вам нужно передать свои типы через c-api (вы выполняете create_my_object или реализуете обратный вызов, который должен принимать пустоту *)
  • Случаи условного владения: Подумайте о строке, которая не выделяет память, когда она создается из строковой литтера, но просто указывает на эти данные. В последнее время вы, вероятно, могли бы использовать std::variant<T*, unique_ptr<T>> вместо этого, но только если вы в порядке с информацией о владении, хранящейся в этом варианте, и принимаете ли вы накладные расходы на проверку того, какой член активен для каждого доступа. Конечно, это имеет значение только в том случае, если вы не можете/не хотите позволить себе накладные расходы, имея два указателя (один владеющий и один не владеющий)
    • Если вы хотите основать свое владение на чем-либо более сложном, чем указатель. Например. вы хотите использовать gsl:: owner, чтобы вы могли легко запросить его размер и иметь все остальные свойства (итерация, rangecheck...). По общему признанию, вы, скорее всего, обернете это в своем собственном классе, так что это может попасть в категорию реализации контейнера.

Ответ 11

Добавляя к другим ответам, есть случаи, когда new/delete имеют смысл -

  • Интеграция с сторонней библиотекой, которая возвращает необработанный указатель и ожидает, что вы вернете указатель на библиотеку после завершения (библиотека имеет свои собственные функции управления памятью).
  • Работа с встроенным устройством с ограниченными ресурсами, где память (RAM/ROM) - это роскошь (даже несколько килобайт). Вы уверены, что хотите добавить в приложение дополнительное время работы (RAM) и скомпилированное (ROM/Overlay), или вы хотите тщательно программировать с помощью нового/удалить?
  • С точки зрения пуризма, в некоторых случаях умные указатели не будут работать интуитивно (из-за их природы). Например, для шаблона построителя вы должны использовать reinterpret_pointer_cast, если вы используете интеллектуальные указатели. Другим случаем является то, что вам нужно отбрасывать базовый тип в производный тип. Вы подвергаете себя опасности, если вы получаете необработанный указатель от умного указателя, бросаете его и помещаете в другой умный указатель и в конечном итоге освобождаете указатель несколько раз.

Ответ 12

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

Контейнеры - хороший удобный способ быстро собирать данные и работать с ним, но реализация использует дополнительную память и дополнительные различия, которые влияют как на память, так и на производительность. Мой недавний эксперимент по замене интеллектуальных указателей другой пользовательской реализацией обеспечил примерно 20% прироста производительности в препроцессоре verilog. Несколько лет назад я сравнивал пользовательские списки и пользовательские деревья с векторами/картами, а также видел прибыль. Пользовательские реализации полагаются на регулярное новое/удаление.

Итак, new/delete полезны в высокопроизводительных приложениях для специализированных структурированных структур данных.

Ответ 13

Вы можете использовать new и delete, если мы хотим создать собственный механизм распределения памяти. Например

1. Использование In-Place new: обычно используется для выделения из предварительно распределенной памяти;

char arr[4];

int * intVar = new (&arr) int; // assuming int of size 4 bytes

2. Использование специальных атрибутов класса: если нам нужен собственный распределитель для наших собственных классов.

class AwithCustom {

public:
    void * operator new(size_t size) {
         return malloc(size);
    }

    void operator delete(void * ptr) {
          free(ptr);
    }
};

Ответ 14

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

Например:

#include <iostream>
#include <memory>

class Base
{
public:
    virtual ~Base() {}

    virtual Base* clone() const = 0;
};

class Foo : public Base
{
public:
    ~Foo() override {}

    // Case A in main wouldn't work if this returned `Base*`
    Foo* clone() const override { return new Foo(); }
};

class Bar : public Base
{
public:
    ~Bar() override {}

    // Case A in main wouldn't work if this returned `Base*`
    Bar* clone() const override { return new Bar(); }
};

int main()
{
    Foo defaultFoo;
    Bar defaultBar;

    // Case A: Can maintain the same type when cloning
    std::unique_ptr<Foo> fooCopy(defaultFoo.clone());
    std::unique_ptr<Bar> barCopy(defaultBar.clone());

    // Case B: Of course cloning to a base type still works
    std::unique_ptr<Base> base1(fooCopy->clone());
    std::unique_ptr<Base> base2(barCopy->clone());

    return 0;
}

Ответ 15

По-прежнему существует возможность использовать malloc/free в С++, так как вы можете использовать new/delete, и любой более высокий уровень обертывания шаблонов памяти STL.

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

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

Это мои эмпирические правила, которые я всегда имею в виду:

std::shared_ptr: автоматическое управление указателями, но из-за подсчета ссылок, которое оно использует для отслеживания указателей доступа, у вас худшая производительность при каждом доступе к этим объектам. Сравнивая простые указатели, я бы сказал, что в 6 раз медленнее. Имейте в виду, вы можете использовать get() и извлечь примитивный указатель и продолжить доступ к нему. Вы должны быть осторожны с этим. Мне нравится, как ссылка с *get(), поэтому худшая производительность на самом деле не является сделкой.

std::unique_ptr Доступ указателя может произойти только в одной точке кода. Поскольку этот шаблон запрещает копирование, благодаря функции r-reference &&, она намного быстрее, чем std::shared_ptr. Поскольку в этом классе все еще есть некоторые накладные расходы, они примерно в два раза медленнее, чем примитивный указатель. Вы получаете доступ к объекту, чем примитивный указатель внутри этого шаблона. Мне также нравится использовать ссылочный трюк здесь, для менее требуемого доступа к объекту.

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

В С++ никто не должен заботиться о malloc и free, но они существуют для устаревшего кода. Они в основном отличаются тем фактом, что они ничего не знают о классах С++, которые с оператором case new и delete отличаются.

Я использую std::unique_ptr и std::shared_ptr в моем проекте Commander Genius повсюду, и я действительно счастлив, что они существуют. С тех пор я не занимаюсь утечками памяти и segfaults. До этого у нас был собственный шаблон смарт-указателя. Поэтому для продуктивного программного обеспечения я не могу рекомендовать их достаточно.

Ответ 16

Другим вариантом использования может быть сторонняя библиотека, возвращающая необработанный указатель, который внутренне покрыт собственным интрузивным подсчетом ссылок (или собственным управлением памятью, который не покрывается каким-либо API/пользовательским интерфейсом).

Хорошим примером является OpenSceneGraph и их реализация osg:: ref_ptr container и osg:: Referenced base class.

Хотя возможно использование shared_ptr, интрузивный подсчет ссылок лучше подходит для графика сцены, например, для использования.

Лично я вижу что-то "умное" на unique_ptr. Это просто область заблокирована и удалена. Хотя shared_ptr выглядит лучше, для него требуются накладные расходы, что во многих случаях неприемлемо.

В общем, мой вариант использования:

При работе с обертками необработанных указателей не STL.

Ответ 17

еще один пример, который еще не упоминался, - это когда вам нужно передать объект через устаревший (возможно асинхронный) C-обратный вызов. Обычно эти вещи принимают указатель на функцию и void * (или непрозрачный дескриптор) для передачи некоторой полезной нагрузки. Пока обратный вызов дает некоторую гарантию того, когда/как/сколько раз он будет вызываться, использование простого метода new- > cast- > callback- > cast- > delete является самым простым решением (хорошо, удаление будет вероятно, управляется уникальным_ptr на сайте обратного вызова, но голый новый все еще существует). Конечно, альтернативные решения существуют, но в этом случае всегда требуется реализация какого-то явного/неявного "менеджера жизненного цикла объекта".

Ответ 18

Я думаю, что это, как правило, хороший прецедент и/или рекомендация:

  • Когда указатель локален для области с единственной функцией.
  • Динамическая память обрабатывается в функции, и вам нужна куча.
  • Вы не передаете указатель и не оставляете область видимости функций.

Код PSEUDO:

#include <SomeImageLibrary>

// Texture is a class or struct defined somewhere else.
unsigned funcToOpenAndLoadImageData( const std::string& filenameAndPath, Texture& texture, some optional flags (how to process or handle within function ) {
    // Depending on the above library: file* or iostream...

    // 1. OpenFile

    // 2. Read In Header

    // 3. Process Header

    // 4. setup some local variables.

    // 5. extract basic local variables from the header
    //    A. texture width, height, bits per pixel, orientation flags, compression flags etc.

    // 6. Do some calculations based on the above to find out how much data there is for the actual ImageData...

    // 7. Raw pointer (typically of unsigned char).

    // 8. Create dynamic memory for that pointer or array.

    // 9. Read in the information from the file of that amount into the pointer - array.

    // 10. Verify you have all the information.

    // 11. Close the file handle.

    // 12. Process some more information on the actual pointer or array itself
    // based on its orientation, its bits per pixel, its dimensions, the color type, the compression type, and or if it exists encryption type.

    // 13. Store the modified data from the array into Your Structure (Texture - Class/Struct).

    // 14. Free up dynamic memory...

    // 15. typically return the texture through the parameter list as a reference

    // 16. typically return an unsigned int as the Texture numerical ID.    
}

Это довольно эффективно; эффективный, не требует использования интеллектуальных указателей; быстро, особенно если вложить функцию. Этот тип функции может быть автономным или даже членом класса. Если шаблон следует за этим, то вполне безопасно использовать новое и удалить или новое [] и удалить [], если все сделано правильно.

EDIT

В вышеупомянутом случае (-ах) выше иногда вам нужны необработанные указатели, и вы хотите их в кучу. Скажем, у вас есть приложение, которое загрузит, скажем, 5000 файлов текстур, 500 файлов моделей, 20 файлов сцен, 500-1000 аудиофайлов. Вы не хотите, чтобы время загрузки было медленным, вы также хотите, чтобы он был "кеш" дружественным. Загрузка текстуры - очень хороший пример наличия указателя на кучу в отличие от стека функций, потому что текстура может быть большой по размеру, превышающей возможности вашей локальной памяти.

В этом контексте вы будете называть эти функции загрузки один раз на объект, но вы будете называть их несколько раз. После того, как вы загрузили и создали свои ресурсы или активы и сохранили их внутри, это когда и где вы хотите использовать контейнеры вместо массивов и интеллектуальных указателей вместо необработанных указателей.

Вы загружаете один актив один раз, но у вас может быть 100 или 1000 экземпляров его. Именно с этими примерами вы предпочитаете использовать контейнеры и использовать интеллектуальные указатели для управления своей памятью в своем приложении над необработанными указателями и массивами. Первоначальная загрузка - это то место, где вы предпочли бы быть ближе к металлу без лишних лишних накладных расходов.

Если вы работали над игрой класса A +, и вы могли бы сохранить свою аудиторию от 15 до 30 секунд или более времени загрузки на экране загрузки, вы попадаете в круг победителей. Да, нужно соблюдать осторожность, и да, у вас все еще могут быть необработанные исключения, но код не является 100% полным доказательством.

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

Многие из этих типов библиотек также работают и работают с raw data, raw memory allocation и т.д., и много раз интеллектуальные указатели не обязательно соответствуют этим типам заданий.

Ответ 19

Если вы хотите создавать многомерные массивы, но не знакомы с синтаксисом С++ 11, например std:: move, или не знакомы с написанием пользовательских удалений для интеллектуальных указателей.