Malloc & placement new vs. new

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

Если я создаю массив объектов, что является убедительной причиной (кроме простоты) для использования:

#define MY_ARRAY_SIZE 10

//  ...

my_object * my_array=new my_object [MY_ARRAY_SIZE];

for (int i=0;i<MY_ARRAY_SIZE;++i) my_array[i]=my_object(i);

над

#define MEMORY_ERROR -1
#define MY_ARRAY_SIZE 10

//  ...

my_object * my_array=(my_object *)malloc(sizeof(my_object)*MY_ARRAY_SIZE);
if (my_object==NULL) throw MEMORY_ERROR;

for (int i=0;i<MY_ARRAY_SIZE;++i) new (my_array+i) my_object (i);

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

delete [] my_array;

а другой, который вы очищаете:

for (int i=0;i<MY_ARRAY_SIZE;++i) my_array[i].~T();

free(my_array);

У меня есть причина убедительной. Призывы к тому, что его С++ (а не C) и, следовательно, malloc и free не должны использоваться, не являются - насколько я могу судить - убедить столько, сколько это догматический. Есть ли что-то, что мне не хватает, что делает new [] выше malloc?

Я имею в виду, насколько я могу судить, вы вообще не можете использовать new [] - для создания массива вещей, у которых нет конструктора без параметров без параметров, тогда как метод malloc могут быть использованы.

Ответ 1

Если вы не хотите, чтобы ваша память была инициализирована неявными вызовами конструктора, и вам просто нужно выделенное выделение памяти для placement new, то отлично использовать malloc и free вместо new[] и delete[].

Причины убедительные для использования new over malloc заключаются в том, что new обеспечивает неявную инициализацию через вызовы конструктора, сохраняя дополнительные memset или связанные вызовы функций post a malloc И что для new вам не нужно проверять значение NULL после каждого выделения, просто включение обработчиков исключений сделает задание, экономя вашу избыточную проверку ошибок в отличие от malloc.
Эти убедительные причины не относятся к вашему использованию.

который эффективен по эффективности, может быть определен только профилированием, нет ничего плохого в том, что вы сейчас используете. На стороне примечания я не вижу веских причин, почему следует использовать malloc над new[].

Ответ 2

У меня есть веская причина.

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

Простой факт: да, вы абсолютно можете делать то, что вы описываете. Нет причин, чтобы то, что вы описываете, не будет функционировать.

Но опять же, вы можете иметь виртуальные функции в C. Вы можете реализовать классы и наследование в простой C, если вы вложите в это время и усилия. Они также полностью функциональны.

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

То же самое касается и этого. Итак, каковы выгоды и потери от ручного управления malloc/free array?

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

Производительность

Вы заявляете следующее:

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

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

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

Хрупкость

Предположим, что для каждого объекта в массиве должен быть специализированный конструктор или что-то, вызывающее его, так что инициализация массива требует такого рода вещей. Но рассмотрите свой код уничтожения:

for (int i=0;i<MY_ARRAY_SIZE;++i) my_array[i].~T();

Для простого случая это нормально. У вас есть переменная macro или const, в которой указано, сколько у вас объектов. И вы перебираете каждый элемент, чтобы уничтожить данные. Это замечательно для простого примера.

Теперь рассмотрим реальное приложение, а не пример. Сколько разных мест вы будете создавать в массиве? Десятки? Сотни? Каждому нужно будет иметь собственный цикл for для инициализации массива. Каждому необходимо будет иметь собственный цикл for для уничтожения массива.

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

И вот важный вопрос: для данного массива, где вы сохраняете размер? Знаете ли вы, сколько предметов вы выделили для каждого создаваемого массива? У каждого массива, вероятно, будет свой собственный способ узнать, сколько предметов он хранит. Таким образом, каждый цикл деструктора должен правильно отображать эти данные. Если он ошибается... бум.

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

Теперь рассмотрим альтернативу:

delete[] my_array;

Это невозможно. Он всегда уничтожит каждый элемент. Он отслеживает размер массива и исключает его. Поэтому гарантируется работа. Он не может работать (пока вы выделили его с помощью new[]).

Конечно, можно сказать, что вы можете обернуть массив в объект. В этом есть смысл. Вы даже можете создать шаблон для элементов типа массива. Таким образом, все коды деструктора одинаковы. Размер содержится в объекте. Возможно, возможно, вы понимаете, что пользователь должен иметь некоторый контроль над тем, как выделяется память, так что это не просто malloc/free.

Поздравляем: вы только что изобрели std::vector.

Вот почему многие программисты на С++ даже не печатают new[].

Гибкость

В вашем коде используется malloc/free. Но позвольте сказать, что я занимаюсь профилированием. И я понимаю, что malloc/free для некоторых часто создаваемых типов слишком дорого. Я создаю для них специальный менеджер памяти. Но как связать все распределения массивов с ними?

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

Если бы я использовал new[]/delete[], я мог бы использовать перегрузку оператора. Я просто обеспечиваю перегрузку для операторов new[] и delete[] для этих типов. Никакой код не должен меняться. Для кого-то гораздо труднее обойти эти перегрузки; им приходится активно пытаться. И так далее.

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

читаемость

Рассмотрим это:

my_object *my_array = new my_object[10];
for (int i=0; i<MY_ARRAY_SIZE; ++i)
  my_array[i]=my_object(i);

//... Do stuff with the array

delete [] my_array;

Сравните это с этим:

my_object *my_array = (my_object *)malloc(sizeof(my_object) * MY_ARRAY_SIZE);
if(my_object==NULL)
  throw MEMORY_ERROR;

int i;
try
{
    for(i=0; i<MY_ARRAY_SIZE; ++i)
      new(my_array+i) my_object(i);
}
catch(...)  //Exception safety.
{
    for(i; i>0; --i)  //The i-th object was not successfully constructed
        my_array[i-1].~T();
    throw;
}

//... Do stuff with the array

for(int i=MY_ARRAY_SIZE; i>=0; --i)
  my_array[i].~T();
free(my_array);

Объективно говоря, какой из них легче читать и понимать, что происходит?

Просто посмотрите на это утверждение: (my_object *)malloc(sizeof(my_object) * MY_ARRAY_SIZE). Это очень низкий уровень. Вы не выделяете массив чего-либо; вы выделяете кучу памяти. Вы должны вручную вычислить размер куска памяти, чтобы он соответствовал размеру объекта * количеству объектов, которые вы хотите. В нем даже есть бросок.

В отличие от этого, new my_object[10] рассказывает историю. new - это ключевое слово С++ для "создания экземпляров типов". my_object[10] - это 10-элементный массив типа my_object. Это просто, понятно и интуитивно понятно. Там нет кастингов, нет вычислений размеров байтов, ничего.

Метод malloc требует изучения того, как использовать malloc идиоматически. Метод new требует просто понимания того, как работает new. Это гораздо менее подробное и гораздо более очевидное, что происходит.

Кроме того, после оператора malloc у вас фактически нет массива объектов. malloc просто возвращает блок памяти, который вы сказали компилятору С++, чтобы сделать вид, является указателем на объект (с литой). Это не массив объектов, потому что объекты на С++ имеют время жизни. И время жизни объекта не начинается до его построения. Ничто в этой памяти еще не вызвало конструктора, и поэтому в нем нет живых объектов.

my_array в этой точке не является массивом; это всего лишь блок памяти. Он не станет массивом my_object, пока вы не построите их на следующем шаге. Это невероятно неинтуитивно для нового программиста; он берет закаленную С++-руку (тот, кто, вероятно, учился у C), чтобы знать, что это не живые объекты, и с ними следует обращаться осторожно. Указатель еще не ведет себя как правильный my_object*, потому что он еще не указывает на какой-либо my_object.

В отличие от вас, у вас есть живые объекты в случае new[]. Объекты были построены; они живые и полностью сформированные. Вы можете использовать этот указатель так же, как и любой другой my_object*.

Fin

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

Ответ 3

Я бы тоже сказал.

Лучший способ сделать это:

std::vector<my_object>   my_array;
my_array.reserve(MY_ARRAY_SIZE);

for (int i=0;i<MY_ARRAY_SIZE;++i)
{    my_array.push_back(my_object(i));
}

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

Ответ 4

Вы переопределили new[]/delete[] здесь, и то, что вы написали, довольно распространено в разработке специализированных распределителей.

Накладные расходы при вызове простых конструкторов потребуют мало времени для сравнения распределения. Это не обязательно "намного эффективнее" - это зависит от сложности конструктора по умолчанию и от operator=.

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

new[]/delete[] являются предпочтительными для удобства. Они вводят немного накладных расходов и могут спасти вас от множества глупых ошибок. Вы достаточно вынуждены убрать эту функциональность и использовать коллекцию/контейнер повсюду, чтобы поддерживать свою собственную конструкцию? Я реализовал этот распределитель - настоящий беспорядок создает функторы для всех вариантов конструкции, которые вам понадобятся на практике. Во всяком случае, у вас часто бывает более точное исполнение за счет программы, которую часто сложнее поддерживать, чем все идиомы, которые все знают.

Ответ 5

ИМХО есть и уродливые, лучше использовать векторы. Просто не забудьте заранее выделить пространство для производительности.

Или:

std::vector<my_object> my_array(MY_ARRAY_SIZE);

Если вы хотите инициализировать значение по умолчанию для всех записей.

my_object basic;
std::vector<my_object> my_array(MY_ARRAY_SIZE, basic);

Или если вы не хотите создавать объекты, но хотите зарезервировать пространство:

std::vector<my_object> my_array;
my_array.reserve(MY_ARRAY_SIZE);

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

my_object* carray = &my_array[0];      
my_object* carray = &my_array.front(); // Or the C++ way

Доступ к отдельным элементам:

my_object value = my_array[i];    // The non-safe c-like faster way
my_object value = my_array.at(i); // With bounds checking, throws range exception

Typedef для симпатичного:

typedef std::vector<my_object> object_vect;

Передайте им функции со ссылками:

void some_function(const object_vect& my_array);

EDIT: В С++ 11 существует также std:: array. Проблема с ним, хотя это размер делается с помощью шаблона, поэтому вы не можете делать разные размеры во время выполнения, и вы не можете передать его в функции, если только они не ожидали такого же размера (или сами функции шаблона). Но это может быть полезно для таких вещей, как буферы.

std::array<int, 1024> my_array;

EDIT2: Также в С++ 11 есть новый emplace_back в качестве альтернативы push_back. Это в основном позволяет вам "перемещать" ваш объект (или конструировать свой объект непосредственно в векторе) и сохраняет вашу копию.

std::vector<SomeClass> v;
SomeClass bob {"Bob", "Ross", 10.34f};
v.emplace_back(bob);
v.emplace_back("Another", "One", 111.0f); // <- Note this doesn't work with initialization lists ☹

Ответ 6

Хорошо, я думал, что, учитывая количество ответов, не было бы никаких оснований для участия... но я думаю, что меня привлекают как другие. Пусть go

  • Почему ваше решение нарушено.
  • С++ 11 новых возможностей для обработки необработанной памяти
  • Простой способ сделать это.
  • Советы

1. Почему ваше решение нарушено

Во-первых, два предоставленных вами фрагмента не эквивалентны. new[] просто работает, твой ужасно терпит неудачу в присутствии Исключения.

Что new[] делает под обложкой, так это то, что она отслеживает количество объектов, которые были сконструированы, так что, если возникает исключение во время вызова третьего конструктора, он правильно вызывает деструктор для 2 уже построенных объектов.

Ваше решение, однако, ужасно не работает:

  • либо вы вообще не обрабатываете исключения (и утомляетесь ужасно)
  • или вы просто пытаетесь вызвать деструкторы на весь массив, даже если он наполовину построен (вероятно, сбой, но кто знает с поведением undefined)

Итак, эти два явно не эквивалентны. Ваш сломан

2. С++ 11 новых возможностей для обработки необработанной памяти

В С++ 11 члены комитета поняли, насколько нам нравится играть с необработанной памятью, и они внедрили средства, которые помогут нам сделать это более эффективно и безопаснее.

Проверьте cppreference <memory>. В этом примере показаны новые лакомства (*):

#include <iostream>
#include <string>
#include <memory>
#include <algorithm>

int main()
{
    const std::string s[] = {"This", "is", "a", "test", "."};
    std::string* p = std::get_temporary_buffer<std::string>(5).first;

    std::copy(std::begin(s), std::end(s),
              std::raw_storage_iterator<std::string*, std::string>(p));

    for(std::string* i = p; i!=p+5; ++i) {
        std::cout << *i << '\n';
        i->~basic_string<char>();
    }
    std::return_temporary_buffer(p);
}

Обратите внимание, что get_temporary_buffer не-throw, он возвращает количество элементов, для которых память фактически была выделена как второй элемент pair (таким образом, .first, чтобы получить указатель).

(*) Или, возможно, не так новичок, как заметил MooingDuck.

3. Простой способ сделать это

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

Знаете ли вы о boost::optional?

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

Ну, ваш прецедент действительно выглядит как std::vector< boost::optional<T> > для меня (или, возможно, deque?)

4. Советы

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

Не забывайте: Не повторяйте себя!

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

И, конечно же, почему бы не воспользоваться преимуществами новых средств С++ 11 при этом:)?

Ответ 7

Вы должны использовать vectors.

Ответ 8

Догматический или нет, это именно то, что ВСЕ контейнер STL выделяет и инициализирует.

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

Если это (как говорят многие люди), "не С++", как может быть реализована эта стандартная библиотека?

Если вы просто не хотите использовать malloc/free, вы можете выделить "байты" только с помощью new char[]

myobjet* pvext = reinterpret_cast<myobject*>(new char[sizeof(myobject)*vectsize]);
for(int i=0; i<vectsize; ++i) new(myobject+i)myobject(params);
...
for(int i=vectsize-1; i!=0u-1; --i) (myobject+i)->~myobject();
delete[] reinterpret_cast<char*>(myobject);

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

Обратите внимание, что, поместив мою первую и последнюю строку в класс myallocator<myobject>, а второй и второй - в класс myvector<myobject>, мы... просто переопределили std::vector<myobject, std::allocator<myobject> >

Ответ 9

То, что вы показали здесь, на самом деле является способом использования распределителя памяти, отличного от общего распределителя системы, - в этом случае вы выделили бы свою память с помощью распределителя (alloc- > malloc (sizeof (my_object))) и затем используйте новый оператор размещения для его инициализации. Это имеет много преимуществ в эффективном управлении памятью и довольно распространено в стандартной библиотеке шаблонов.

Ответ 10

Если вы пишете класс, который имитирует функциональные возможности std::vector или нуждается в контроле над распределением памяти/созданием объекта (вставка в массив/удаление и т.д.) - это путь. В этом случае это не вопрос "не вызывающий конструктор по умолчанию". Речь идет о возможности "распределять необработанную память, memmove старых объектов, а затем создавать новые объекты по адресам старых", вопрос о возможности использования какой-либо формы realloc и т.д. Несомненно, пользовательское распределение + размещение new более гибкое... Я знаю, я немного пьян, но std::vector для сиси... Об эффективности - можно написать собственную версию std::vector, которая будет максимально быстрым (и, скорее всего, меньше sizeof()) с большинством используемых 80% функциональных возможностей std::vector, возможно, менее 3 часов.

Ответ 11

my_object * my_array=new my_object [10];

Это будет массив с объектами.

my_object * my_array=(my_object *)malloc(sizeof(my_object)*MY_ARRAY_SIZE);

Это будет массив размером ваших объектов, но они могут быть "сломанными". Если у вашего класса есть виртуальные функции, например, вы не сможете их назвать. Обратите внимание, что это не просто данные вашего члена, которые могут быть непоследовательными, но весь объект является "сломанным" (в отсутствии лучшего слова)

Я не говорю, что неправильно делать вторую, пока вы это знаете.