Можно ли переместить std::array в std::vector?

Это вопрос о взаимодействии памяти стека и памяти кучи и частном случае перехода от стека к куче через классы std::array и std::vector.

В принципе std::array<T> можно рассматривать как указатель на первые элементы, а также некоторую информацию о времени компиляции о размере массива. Возможно ли иметь конструктор std::vector<T>, который учитывает этот факт и пытается переместить содержимое array в vector, просто скопировав указатель.

Пример использования: у него есть функция, которая возвращает std::array<double, >

std::array<double, 20> fun(){...};

но позже решается назначить его std::vector без необходимости копирования поэлементно.

std::vector<double> v = fun(); // not working code

Прямо сейчас нужно сделать

std::array<double, 20> tmp = fun();
std::vector<double> v(tmp.begin(), tmp.end());

Что на самом деле выполняет некоторую избыточную работу, в которой не было бы необходимости, если бы это было возможно std::vector<double> v(std::move(tmp)); \\ not working code.

Схема памяти std::vector и std::array одинакова, так что это не препятствие.

Я понимаю, что основным препятствием может быть то, что std::array элементов находятся в стеке, а std::vector элементов находятся в куче. Понятно, что даже если написать конструктор перемещения для std::vector, все равно память из стека будет безвозвратно уничтожена.

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

Есть ли способ переместить память из стека в кучу (что бы это ни значило) и можно ли это объединить с конструктором перемещения?

Или если std::vector может иметь в принципе конструктор перемещения из std::array?

MWE:

#include<array>
#include<vector>

std::array<double, 20> fun(){return {};} // don't change this function

int main(){
    std::array<double, 20> arr = fun(); // ok
    std::vector<double> v(arr.begin(), arr.end()); // ok, but copies and the allocation is duplicated
    std::vector<double> v2 = fun(); // not working, but the idea is that the work is not duplicated
}

person alfC    schedule 01.12.2015    source источник
comment
Что, если вы переместите массив в вектор, а затем push_back() новый элемент?   -  person Antonio Pérez    schedule 01.12.2015
comment
@AntonioPérez, хорошая мысль, но я думаю, что std::vector будет вести себя как обычно и перераспределит (зарезервирует) больше памяти (возможно, где-то еще в памяти). Оптимизация будет потеряна, но вопрос не в этом.   -  person alfC    schedule 01.12.2015
comment
Возможно, это было бы возможно в ограниченном виде для vector классов, использующих оптимизацию малого вектора. boost.org/doc/ libs/1_60_0/doc/html/container/ . AFAIK, который по-прежнему исключает std::vector, который не использует эту оптимизацию (но std::basic_string использует). Возможно также некая специализация std::vector с особым (фальшивым) аллокатором, как в одном из комментариев ниже.   -  person alfC    schedule 14.01.2016


Ответы (2)


Есть ли способ переместить память из стека в кучу (что бы это ни значило) и можно ли это объединить с конструктором перемещения?

Мне лично нравится фраза «что бы это ни значило». Давайте немного поразмыслим над этим. Перемещение чего-либо из стека в кучу может внезапно означать, что эта часть стека внезапно становится помеченной как область, выделенная в куче, и подлежит регулярному уничтожению.

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

Проиллюстрировать:

|                      |
|----------------------|
| stack block 1        |
|----------------------|
| your vector          |
|----------------------|
| stack block 2        |
|----------------------|
|-                    -|

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

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

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

person Bartek Banachewicz    schedule 01.12.2015
comment
Отличный ответ. Я думаю, все сводится к стеку, а куча может общаться только путем копирования. Интересной деталью является то, что в случае использования память из массива находится на вершине стека при передаче (копировании или перемещении) в `std::vector. - person alfC; 01.12.2015
comment
Да, если вектор вырастет позже, оптимизация будет потеряна, потому что память придется перераспределять. Но только тогда, если он останется прежним, то все еще в порядке. - person alfC; 01.12.2015
comment
Даже если размер вектора составляет несколько мегабайт, я полагаю, контроллер памяти может просто поменять местами некоторые страницы, и ему не нужно будет физически посылать электрические сигналы, соответствующие битам данных. Я так не думаю. так. К тому времени, когда вектор начнет копировать биты, страницы уже будут выделены malloc (или подобным образом), и при резервировании этих страниц ОС не имеет ни малейшего представления о том, что они в конечном итоге будут содержать те же данные, что и некоторая часть стека. Вы также делаете предположения о выравнивании по границам страницы и размещении нескольких мегабайт в стеке. - person Jonathan Wakely; 01.12.2015
comment
Также возможно иметь несмежные стеки (поиск разделенных стеков или сегментированных стеков), но в этом случае это не очень поможет. - person Jonathan Wakely; 01.12.2015

Похоже, вы хотите сказать std::vector использовать данные std::array в качестве базового буфера, по крайней мере, до тех пор, пока ему не потребуется какое-то перераспределение.

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

Что вы можете сделать, так это создать std::vector с помощью std::move_iterator. s, чтобы переместить содержимое из файла std::array. Конечно, это не будет иметь значения для арифметических типов, но для логически больших объектов, которые дешево перемещать, можно избежать копирования большого количества данных:

std::array<BigThing, 20> a = fun();
std::vector<BigThing> b { std::make_move_iterator(a.begin()),
                          std::make_move_iterator(a.end())) };
person TartanLlama    schedule 01.12.2015
comment
Ну, вы можете использовать собственный распределитель, например short_alloc Говарда Хиннанта. Основная проблема, которую я вижу, заключается в том, что vector управляет своим собственным размером, и вам нужны хаки для его инициализации с уже заполненным массивом (например, resize плюс настраиваемый allocator::construct, который не выполняет никакой инициализации при запросе значения-init). - person dyp; 01.12.2015
comment
@dyp, да, я думал о пользовательском распределителе, который будет имитировать выделение памяти из предоставленного массива. Проблема (возможно, неизбежная) заключается в том, что в любой момент (в зависимости от масштаба) сам std::array может исчезнуть. (Я не упомянул взлом пользовательского распределителя, чтобы не усложнять вопрос). Одним из вариантов может быть каким-то образом заставить вызов деструктора массива исчезнуть, но даже если бы это было возможно, это нарушило бы некоторые ограничения в поведении стека (см. ответ @Bartek). - person alfC; 01.12.2015
comment
Я не знал о move_iterator en.cppreference.com/w/cpp/iterator/move_iterator - person alfC; 01.12.2015
comment
@dyp возможно ли это без нарушения контракта распределителя? - person TartanLlama; 01.12.2015
comment
@TartanLlama, каким будет контракт распределителя? (Честный вопрос: может ли быть распределитель, который не выделяет, а просто возвращает существующий указатель?) - person alfC; 01.12.2015
comment
@alfC существует множество требований к тому, как должны вести себя распределители, изложенные в [allocator.requirements] спецификации. Я вообще не знаком с требованиями, отсюда и вопрос. - person TartanLlama; 01.12.2015
comment
Я помню, что был по крайней мере один вопрос SO о более быстром изменении размера с использованием default-init вместо value-init. В настоящее время контракт allocator::construct в любом случае недоопределен, я думаю, что он действителен, если вы возвращаете уже построенный объект. - person dyp; 01.12.2015
comment
Нашел: stackoverflow.com/a/21028912 см. также: stackoverflow.com/a/15975738 - person dyp; 01.12.2015