malloc и размещение новых против новых - PullRequest
37 голосов
/ 22 января 2012

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

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

#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 ++ (а не C) и, следовательно, malloc и free не должны использоваться, не - насколько я могу судить - неотразимо столько, сколько это догматический . Я что-то упускаю, что делает new [] превосходящим malloc?

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

Ответы [ 11 ]

64 голосов
/ 22 января 2012

Я отсутствую по веской причине.

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

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

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

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

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

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

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

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

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

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

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

Хрупкость

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

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

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

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

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

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

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

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

delete[] my_array;

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

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

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

Именно поэтому многие программисты на C ++ даже не набирают 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 - ключевое слово C ++ для «создания экземпляров типов». my_object[10] - это массив из 10 элементов типа my_object. Это просто, очевидно и интуитивно понятно. Там нет приведения, нет вычисления байтов, ничего.

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

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

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

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

Fin

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

36 голосов
/ 22 января 2012

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

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

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

19 голосов
/ 22 января 2012

Я бы сказал, что нет.

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

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));
}

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

10 голосов
/ 22 января 2012

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

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

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

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

6 голосов
/ 22 января 2012

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

Либо:

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.)

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);

РЕДАКТИРОВАТЬ: В C ++ 11 также есть std :: array.Проблема с ним в том, что его размер выполняется с помощью шаблона, поэтому вы не можете создавать различные размеры во время выполнения и не можете передавать его в функции, если они не ожидают такого же размера (или сами функции шаблона).Но это может быть полезно для таких вещей, как буферы.

std::array<int, 1024> my_array;

EDIT2: также в C ++ 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 ☹
5 голосов
/ 22 января 2012

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

  1. Почему ваше решение не работает
  2. C ++ 11 новых средств для обработки необработанной памяти
  3. Более простой способ сделать это
  4. Советы

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

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

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

Однако ваше решение ужасно терпит неудачу:

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

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

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

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

Проверьте краткую справку <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 - это не бросок, он возвращает количество элементов, для которых память фактически была выделена как второй член pair (таким образом, .first для получения указателя).

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

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

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

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

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

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

4.Советы

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

Не забудьте: Не повторяйте себя!

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

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

3 голосов
/ 22 января 2012

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

2 голосов
/ 22 января 2012

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

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

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

Если вы просто не хотите использовать 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> >

1 голос
/ 22 января 2012

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

1 голос
/ 22 января 2012

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

...