С++ размещение нового в домашнем векторном контейнере

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

Я сделал векторный контейнер (не мог использовать stl из-за проблем с памятью), который раньше использовал только оператор = для push_back*, и как только я наткнулся на новое размещение, я решил ввести в него дополнительный «emplace_back»**.

*(ожидается, что T::operator= будет иметь дело с управлением памятью)

** (имя взято из аналогичной функции в std::vector, с которой я столкнулся позже, исходное имя, которое я дал, было беспорядком).

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

Это, конечно, упрощенный код, без итераторов и без расширенной функциональности, но в нем есть смысл:

template <class T>
class myVector {
public :
    myVector(int capacity_) {
        _capacity = capacity_;
        _data = new T[_capacity];
        _size = 0;
    }

    ~myVector() {
        delete[] _data;
    }

    bool push_back(T const & t) {
        if (_size >= _capacity) { return false; }
        _data[_size++] = t;
        return true;
    }

    template <class... Args>
    bool emplace_back(Args const & ... args) {
        if (_size >= _capacity) { return false; }
        _data[_size].~T();
        new (&_data[_size++]) T(args...);
        return true;
    }

    T * erase (T * p) {
        //assert(/*p is not aligned*/);
        if (p < begin() || p >= end()) { return end(); }
        if (p == &back()) { --_size; return end(); }
        *p = back();
        --_size;
        return p;
    }

    // The usual stuff (and more)
    int capacity()          { return _capacity;             }
    int size()              { return _size;                 }
    T * begin()             { return _data;                 }
    T * end()               { return _data + _size;         }
    T const * begin() const { return _data;                 }
    T const * end()   const { return _data + _size;         }
    T & front()             { return *begin();              }
    T & back()              { return *(end() - 1);          }
    T const & front() const { return *begin();              }
    T const & back() const  { return *(end() - 1);          }
    T & operator[] (int i)  { return _data[i];              }
    T const & operator[] (int i) const { return _data[i];   }
private:
    T * _data;
    int _capacity;
    int _size;
};

Спасибо


person elad    schedule 24.11.2015    source источник
comment
Вы будете вызывать деструктор неинициализированного T по крайней мере   -  person James    schedule 25.11.2015
comment
Почему? (или.. где?) все элементы инициализируются пустым конструктором (T::T()) при использовании new[] внутри конструктора myVector::myVector(int) (только)...   -  person elad    schedule 25.11.2015
comment
Вызов new T[ n ]; не создает элементы; он только выделяет память.   -  person user2296177    schedule 25.11.2015
comment
В чем собственно вопрос или проблема? Размещение new создает объект «на месте» в памяти, указанной в качестве параметра. Больше ничего. Ваше размещение нового конструктора выглядит хорошо. С другой стороны, я бы добавил ссылку на rvalue и std::forward аргументы. Комментарий о «не построении» элементов неверен. Он вызывает конструкторы по умолчанию   -  person Adrian Lis    schedule 25.11.2015
comment
@user2296177 user2296177 это неправда, new по умолчанию будет создавать каждый элемент.   -  person Mark Ransom    schedule 25.11.2015
comment
@MarkRansom Вы правы. Я проверил это перед комментированием и получил неинициализированные значения. Я не должен был предполагать.   -  person user2296177    schedule 25.11.2015
comment
@user2296177 user2296177 может быть исключение в случае типа POD. См. stackoverflow.com/questions/8256293/   -  person Mark Ransom    schedule 25.11.2015


Ответы (1)


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

Для operator new[] по сравнению с новым размещением это действительно плохо (как в типичном аварийном типе неопределенного поведения), если вы смешиваете две стратегии вместе.

Основной выбор, который вам обычно приходится делать, — использовать тот или иной. Если вы используете operator new[], то вы заранее конструируете все элементы на всю емкость контейнера и перезаписываете их в методах типа push_back. Вы не уничтожаете их при удалении в таких методах, как erase, просто оставляете их там и настраиваете размер, перезаписываете элементы и т.д. Вы создаете и выделяете несколько элементов за один раз с помощью operator new[], а также уничтожаете и освобождаете их все за один раз с помощью operator delete[].

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

Первое, что нужно понять, хотите ли вы начать скручивать свои собственные векторы или другие совместимые со стандартом последовательности (которые не являются просто связанными структурами с одним элементом на узел) таким образом, чтобы фактически уничтожать элементы при их удалении, создавать элементы (а не просто перезаписать их) при добавлении состоит в том, чтобы разделить идею выделения памяти для контейнера и создания элементов для него на месте. Так что, как раз наоборот, в данном случае размещение нового — это неплохо. Это фундаментальная необходимость для достижения общих качеств стандартных контейнеров. Но мы не можем смешивать его с operator new[] и operator delete[] в этом контексте.

Например, вы можете выделить память для хранения 100 экземпляров T в reserve, но вы также не хотите создавать их по умолчанию. Вы хотите сконструировать их в таких методах, как push_back, insert, resize, fill ctor, range ctor, copy ctor и т. д. -- методах, которые на самом деле добавляют элементы, а не только возможность их хранения. Вот почему нам нужно новое placement.

В противном случае мы теряем универсальность std::vector, которая позволяет избежать построения элементов, которых нет, может копировать конструкцию в push_backs, а не просто перезаписывать существующие с помощью operator= и т. д.

Итак, начнем с конструктора:

_data = new T[_capacity];

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

Вместо этого вы хотите что-то вроде этого:

_data = static_cast<T*>(malloc(_capacity * sizeof(T)));

Это просто дает нам необработанный кусок байтов.

Во-вторых, для push_back вы делаете:

_data[_size++] = t;

Это попытка использовать оператор присваивания и, после нашей предыдущей модификации, для неинициализированного/недопустимого элемента, который еще не создан. Итак, мы хотим:

new(_data + _size) T(t);
++size;

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

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

if (p == &back()) { --_size; return end(); }

... должно быть больше похоже на:

if (p == &back())
{
    --size;
    (_data + _size)->~T();
    return end(); 
}

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

Ваш деструктор делает это:

~myVector() {
   delete[] _data;
}

Но опять же, это UB, когда мы используем этот подход. Мы хотим что-то более похожее на:

~myVector() {
   for (int j=0; j < _size; ++j)
      (_data + j)->~T();
   free(_data);
}

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

Но это должно помочь вам начать правильное использование размещения new в структуре данных против некоторого распределителя памяти (malloc/free в этом примерном случае).

Последний, но тем не менее важный:

(не удалось использовать stl по причинам памяти)

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

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

В итоге мне пришлось заново изобретать некоторые vectors и векторные контейнеры по причинам ABI (мы хотели, чтобы контейнер, который мы могли передавать через наш API, гарантированно имел один и тот же ABI независимо от того, какой компилятор использовался для создания плагина). Даже тогда я бы предпочел просто использовать std::vector.

Обратите внимание, что если вы просто хотите взять под контроль то, как vector выделяет память, вы можете сделать это, указав свой собственный распределитель с совместимым интерфейсом. Это может быть полезно, например, если вам нужен vector, который выделяет 128-битную выровненную память для использования с выровненными инструкциями перемещения с использованием SIMD.

person Community    schedule 25.11.2015
comment
+1 за последний комментарий. Я бы понял, делая это в учебных целях, но очевидно, что он получит что-то гораздо худшее, чем std::vector, глядя на ваши подходы. Не стоит проблем, я думаю. - person Adrian Lis; 25.11.2015
comment
Вы можете просто сделать new char[_capacity * sizeof(T)], malloc не нужно. - person emlai; 25.11.2015
comment
@zenith Совершенно верно, хотя обычно, когда мы используем новое размещение для какой-либо пользовательской структуры данных, это часто включает какой-то низкоуровневый распределитель, который может просто возвращать void *, например. Я подумал, что это может более непосредственно сопоставляться с тем, что ОП может захотеть делать с такими вещами. - person ; 25.11.2015
comment
Вы также можете упомянуть функции распределения ::operator new. en.cppreference.com/w/cpp/memory/new/operator_new - person user2296177; 25.11.2015
comment
@user2296177 user2296177 Это хорошая идея, хотя я не был уверен, что OP имел в виду под причинами памяти, чтобы перегрузить стандартное поведение распределения (в этом случае интерфейс std::allocator также может представлять интерес) с помощью пользовательского распределения или просто компактности памяти, например Я проголосовал за ваш комментарий, хотя на случай, если это представляет интерес. - person ; 25.11.2015
comment
Что касается последнего комментария - как насчет того, хотите ли вы использовать std::map или std::list в сегменте разделяемой памяти? Распределитель шаблонов используется только для типа значения, а не для указателей, поэтому он не работает. По этой причине межпроцессное повышение уровня предоставляет контейнеры, а не только распределители. - person Jerry Jeremiah; 25.11.2015
comment
Да, то, как стандартная библиотека работает с распределителями, довольно раздражает и ограничивает во многих сценариях. Но иногда есть несколько основных потребностей, когда указание пользовательского распределителя для стандартного контейнера может удовлетворить потребность. - person ; 25.11.2015
comment
@zenith malloc возвращает максимально выровненную память; new char[...] нет. Я подозреваю, что ответ new aligned_storage_t<sizeof(T),alignof(T)>[N]. - person Yakk - Adam Nevraumont; 25.11.2015
comment
@Ike, спасибо за усилия и длинный ответ, но это не должно было быть обзором кода (это даже не настоящий код), и вы не ответили на мой вопрос - было ли здесь какое-либо нарушение памяти? Я не знаю, кто мы в, мы не хотим этого, вы изменили мой конструктор без объяснения причин, а затем заставили изменить все остальное. Я понимаю, как изменение ctor, как вы, вызвало другие изменения, но я действительно хочу, чтобы это было так. - person elad; 25.11.2015
comment
Использование operator= предназначено для обратной совместимости, а что касается использования std::vector - я не могу позволить, чтобы кто-либо, использующий его, забыл использовать резерв и перераспределение/перераспределение памяти. Что касается проблем с выравниванием, я должен признать, что не знал об этом. new T[n] возвращает выровненную память? Что касается операторов перемещения, std::forward и т. д. - это вещи, с которыми я пока не знаком, но, честно говоря, я не думаю, что они мне нужны для моих целей, и я не хочу пока углубляться, если только это действительно необходимая причина, как вы сказали, это довольно раздражает, и я мог бы добавить, что это уродливо и слишком сложно. - person elad; 25.11.2015
comment
Я хотел бы, чтобы это было максимально просто, даже если не супер-общее, и охватывает случаи, с которыми я никогда не столкнусь, и я только хочу знать, делаю ли я что-то, что по сути неправильно. - person elad; 25.11.2015
comment
Кроме того, мы не хотим, чтобы это было nosism. В основном это относится к автору. - person Dan; 25.11.2015
comment
Добавлен ответ, объясняющий, почему именно ctor был несовместим с любым использованием нового размещения, а также почему std::vector и тому подобное избегают использования operator new[] там. - person ; 25.11.2015
comment
Я согласен с тем, что это лучше, но я должен был сделать это, чтобы поддержать то, что другие написали и использовали до меня. У меня нет намерения быть совместимым с stl, а скорее совместимым с существующим плохим дизайном. Старый, широко распространенный вектор (который использует malloc и бесплатно, но не новое размещение) не позволяет копировать конструктор для себя, а только оператор = ( Я знаю, что в принципе это может быть явным вместо этого) и, таким образом, для хранения элементов, которые содержат в них старый вектор, мне пришлось не требовать T::T(T const&) , который потребовал бы, чтобы T определил его, а скорее T::operator =(T const&), который будет определен по умолчанию. - person elad; 25.11.2015
comment
В принципе, если вы хотите использовать новое размещение и иметь такие методы, как push_back, использовать ctor копирования вместо оператора присваивания, иметь такие методы, как стирание, фактически уничтожать элементы, а не просто хранить их в контейнере и т. д., это в основном сводится к использованию размещения нового и ручного вызовы dtors - которые могут быть, а могут и не быть тем, что вы хотите. Извиняюсь - возможно, то, как я описал вещи, было немного любопытным, но я имел в виду, что это то, что мы хотим, предполагая, что вы хотите что-то похожее на стандартные контейнеры (которые используют новое размещение). - person ; 25.11.2015
comment
Теперь об использовании new T[n], это совершенно безопасно с точки зрения выравнивания. Он будет соответствующим образом выровнен в зависимости от того, что такое T. Отличие заключается в том, что если мы действительно используем новое размещение, то new T[n] создаст n элементов. Поэтому, когда мы избегаем этого, нам нужно использовать распределитель, который максимально выравнивается, как malloc, или начинает включать такие вещи, как aligned_storage_t, чтобы обеспечить правильное выравнивание для T. - person ; 25.11.2015
comment
Теперь я вижу, что вы удалили единственный ответ, который вы дали на мой вопрос =), так что спасибо. Все, что я хотел, это понять, почему new[], затем new(), затем delete[] неправильно, потому что кто-то сказал это где-то в Интернете, но если вы говорите, что это хорошо, я верю вам на слово.. о советах по хорошей практике, к которым вы в основном обращались, опять же, я согласен, но у меня нет такой степени свободы.. Спасибо.. - person elad; 25.11.2015
comment
@elad Извините, я удалил здесь пару комментариев, но только потому, что включил их в ответ (не хотел слишком много спамить в этой теме - вы можете найти их там). Что касается вопроса, а что именно? Мне было трудно точно определить, что это было, поскольку название вопроса связано с размещением new в самодельном векторе (поэтому в основном я писал о том, как написать самодельный контейнер, используя его). - person ; 25.11.2015
comment
@elad О, я думаю, что главное, что выглядит как вопрос, это то, что placement new плохо (это в обновленном ответе), и почему здесь на самом деле обычно необходимо реализовать эти контейнеры (по крайней мере, таким образом, чтобы избежать перезаписи, а не уничтожить элементы в момент их добавления/удаления). - person ; 25.11.2015
comment
Однако основной способ взглянуть на это — не требование operator=. Как правило, это не самая запутанная часть (предоставление оператора = не самое сложное). Главное спросить: если я сотру элементы из своего контейнера, должны ли они быть уничтожены или нет? Обычно ответ положительный, иначе вызов таких методов, как стирание, никогда ничего не уничтожит, пока вы не уничтожите весь контейнер или не вызовете clear или что-то в этом роде. Именно здесь нам больше всего нужно размещать новое — не только для того, чтобы избежать копирования и перезаписи материала, но и для того, чтобы убедиться, что он будет уничтожен в нужное время. - person ; 25.11.2015
comment
@Ike, чтобы уточнить, вопрос заключался в том, представляет ли реальная опасность (уровень утечки памяти) использование размещения new в выделенной (и пустой) памяти с использованием new T[n] и/или с использованием простого удаления [] после. Код имеет значение, но он здесь только для того, чтобы дать контекст. Я знал и знаю, что это меньше, чем лучшая практика, поэтому, если сам дизайн не представляет реальной опасности, он здесь не имеет значения (хотя ваш ответ, безусловно, может помочь мне и другим узнать о хорошем пути). - person elad; 25.11.2015
comment
@elad Я вижу - для operator new[] по сравнению с новым размещением это действительно плохо (как в случае аварийного, неопределенного поведения), если вы смешиваете две стратегии вместе. Главный выбор, который вы должны сделать, это использовать тот или иной. Если вы используете operator new[], то вы заранее конструируете все элементы на всю емкость контейнера и перезаписываете их. Вы не уничтожаете их при удалении, а просто оставляете их там и корректируете размер, перезаписываете элементы и так далее. - person ; 25.11.2015
comment
@elad Итак, да, учитывая то, что вы мне говорили, может быть, уместно избегать размещения нового напрямую. Вы должны либо принять идею о том, что выделение памяти для контейнера и создание/уничтожение элементов — это два отдельных и несвязанных процесса, либо соединить их вместе и избегать прямого размещения новых и ручных вызовов деструктора в пользу только operator new[] и operator delete[]. - person ; 25.11.2015
comment
@Ike, забыл упомянуть (но это в коде), каждый раз, когда я использую новое размещение, я сначала уничтожаю старый элемент. это по-прежнему серьезная проблема? кроме того, я могу уничтожать элементы только при необходимости или в конце прогона. Это не реальная опасность, если вы не будете делать странные вещи с вектором, и я могу это предположить. - person elad; 25.11.2015
comment
@Ike, одна вещь, которую я не понял, это ваше высказывание: Отличие заключается в том, что если мы используем новое размещение, то новое T[n] создаст n элементов. можешь уточнить? это то, что я делаю? Благодарность - person elad; 25.11.2015
comment
@elad Я добавил пару абзацев вверху ответа о разнице. Извините, что продолжаю указывать вам на обновленные ответы. - person ; 25.11.2015
comment
@elad "I destruct the old element first." Где это становится странным, я думаю, это то, как вы смешивали operator new[] и новое размещение вместе. В этом случае, с технической точки зрения, вам нужно уничтожить существующий элемент (который был создан operator new[]) до его создания на месте с новым размещением. Но, как правило, мы не должны смешивать эти две стратегии — обычно, если вы используете новое размещение, не должно быть никакого элемента, созданного для памяти, в которой вы создаете элемент на месте. - person ; 25.11.2015
comment
@elad Я бы очень, очень настоятельно рекомендовал (например, даже если вы можете избежать сбоев, код будет похож на стопку карт), если вы используете здесь operator new[] и operator delete[], чтобы избежать использования новых вызовов деструктора размещения и ручных вызовов. Просто слишком сложно смешивать их вместе, поскольку эти операторы хотят выделять/освобождать и создавать/уничтожать множество элементов для вас оптом, а мы копаемся и вручную берем частичный контроль (но без полного контроль, который нам обычно нужен, чтобы сделать это правильно). - person ; 25.11.2015
comment
@ike, спасибо, я прочитал редактирование, но вы продолжаете использовать такие фразы, как мы хотим или мы не хотим, поэтому еще раз, пожалуйста, не думайте, что я хочу избежать лишние конструкции либо лишены разрушений, либо стремятся к общему замыслу. Я просто не хочу утечек памяти или неопределенного поведения... мой код может вызвать такое? Уточнение по поводу выравнивания (я просил 2 комментария выше) тоже было бы неплохо.. - person elad; 25.11.2015
comment
@elad В этом случае давайте начнем с того, что забудем о размещении new и ручных вызовах деструктора - это то, что мы должны сделать, если вам это не нужно. В этом случае просто используйте operator new[]/delete[] для управления строительством и разрушением. Используйте operator= для перезаписи элементов на месте (вместо создания/уничтожения на месте), и вы должны быть установлены. Мы избегаем ошибок при смешивании этих двух методов при условии, что мы не смешиваем. - person ; 25.11.2015
comment
@elad Что касается выравнивания, new T[n] обеспечивает правильное выравнивание для элементов типа T. Но, конечно, если мы используем new char[n] для выделения памяти для Foos, мы можем столкнуться с опасностью неправильного выравнивания. Вот где aligned_storage_t может пригодиться. - person ; 25.11.2015
comment
@Ike, причина, по которой я это делаю, заключается в предположении, что T::T() очень быстрый, а T::operator= очень медленный, что имеет место, например, для классов, которые сами содержат контейнеры. Я не хочу создавать новый элемент снаружи, а затем передавать его через присваивание, поэтому я ввел функцию emplace_back. Я знаю, что выполнение присваивания, похожего на перемещение, вероятно, должно охватывать это, но я все еще недостаточно знаком со ссылками на перемещение и r-значение, плюс я не всегда могу изменять объекты, с которыми имею дело, поэтому я делаю все возможное. Я могу дать их такими, какие они есть. - person elad; 25.11.2015
comment
в любом случае, я провел несколько тестов и понимаю, что мой код в порядке по стандартам вопроса. Спасибо за уделенное время. - person elad; 25.11.2015
comment
@elad Да, проблема скорости, которую вы упомянули в дополнение к общности (не требующей T предоставить operator=), именно поэтому авторы стандартов используют новое размещение. Но вы теряете преимущество в скорости, если используете operator new[], потому что тогда вам придется не только копировать конструкцию, но и уничтожать уже построенный элемент, созданный operator new[]. Обычно медленнее уничтожить элемент, а затем создать новый, чем просто перезаписать его. Однако, если вы используете новую стратегию размещения в ответе, вы можете избежать избыточного строительства/разрушения и построить элементы на месте. - person ; 25.11.2015
comment
@elad Если вы действительно заинтересованы в том, чтобы избежать operator= и всего такого, я могу построить вам минимальный пример чего-то простого (например, стека), в котором используется размещение new для push-уведомлений и ручные вызовы деструктора для pops как часть ответа. Будет ли это полезно? Я могу сделать это очень просто. - person ; 25.11.2015
comment
@Ike, спасибо, но я знаю, как это сделать (кроме && / move, специального выравнивания и т. д.). Моя ситуация такова, что мне нужно иметь operator= для push_back (это означает, что это потребуется от T, а не от CopyCtor), но я хотел расширить свободу для T, которые имеют и хотят этого. Если вам так не терпится опубликовать этот пример, будьте моим гостем, я уверен, что другим он может быть полезен, но я сомневаюсь, что узнаю что-то new =) (опять же, если вы сделаете его простым и простым). Вы также можете удивить меня .. - person elad; 25.11.2015
comment
@elad Я очень неохотно, но полагаю, если вы будете осторожны, вы можете вызвать деструктор, а затем скопировать и создать элемент на месте с помощью operator new(mem) в качестве средства перезаписи, несмотря на использование operator new[]. Это очень необычная вещь, и необходимость замены operator= компенсируется необходимостью избыточного уничтожения существующего элемента и создания нового, но если ваши конкретные потребности вынуждают вас пойти по этому пути, это может быть выполнимо. - person ; 25.11.2015
comment
@elad Одна вещь, которую я рекомендую, если вы разрабатываете эти контейнеры, — это протестировать определенный пользователем тип, например myVector‹Foo›. И заставьте Foo увеличивать статический/глобальный счетчик каждый раз, когда он создается (копировать построенный/созданный по умолчанию) и уменьшать его, когда он уничтожается... и убедитесь в своем тесте, что выполнение кучи вещей с myVector, а затем его уничтожение приводит к счетчику экземпляров 0, а не к отрицательному/положительному.. так как это триповая часть - убедитесь, что вызовы конструктора и вызовы деструктора симметричны, особенно если вы смешиваете operator new[] и размещение new. - person ; 25.11.2015
comment
Комментарии не для расширенного обсуждения; этот разговор был перешел в чат. - person Madara's Ghost; 25.11.2015