Как std :: vector поддерживает непрерывную память для пользовательских объектов неизвестного размера - PullRequest
4 голосов
/ 02 апреля 2019

Я борюсь с правильной ментальной моделью и пониманием std::vector.

Что я думал, что знал

Когда вы создаете вектор типа T, а затем резервируете N элементов длявектор, компилятор в основном находит и резервирует непрерывный блок памяти размером N * sizeof(T) байт.Например,

// Initialize a vector of int
std::vector<int> intvec;

// Reserve contigious block of 4 4-byte chunks of memory
intvec.reserve(4);  // [ | | | ]

// Filling in the memory chunks has obvious behavior:
intvec.push_back(1);  // [1| | | ]
intvec.push_back(2);  // [1|2| | ]

Тогда мы можем получить доступ к любому элементу во время произвольного доступа, потому что, если мы запрашиваем k-й элемент вектора, мы просто начинаем с адреса памяти начала вектора, а затем«прыжок» k * sizeof(T) байт, чтобы добраться до k-го элемента.

Пользовательские объекты

Моя интеллектуальная модель ломается для пользовательских объектов неизвестного / переменного размера.Например,

class Foo {

public:
    Foo() = default;
    Foo(std::vector<int> vec): _vec{vec} {}

private:
    std::vector<int> _vec;
};

int main() {

    // Initialize a vector Foo
    std::vector<Foo> foovec;

    // Reserve contigious block of 4 ?-byte chunks of memory
    foovec.reserve(4);  // [ | | | ]

    // How does memory allocation work since object sizes are unkown?
    foovec.emplace_back(std::vector<int> {1,2});        // [{1,2}| | | ]
    foovec.emplace_back(std::vector<int> {1,2,3,4,5});  // [{1,2}|{1,2,3,4,5}| | ]

    return 0;
}

Поскольку мы не знаем размер каждого экземпляра Foo, как foovec.reserve() выделяет память?Кроме того, как можно достичь времени произвольного доступа, мы не знаем, как далеко «прыгнуть», чтобы добраться до k-го элемента?

Ответы [ 4 ]

9 голосов
/ 02 апреля 2019

Ваша концепция размера ошибочна. std::vector<type> имеет известный во время компиляции размер пространства, которое он собирается занять. Он также имеет размер времени выполнения, который он может использовать (он выделяется во время выполнения, а вектор содержит указатель на него). Вы можете изобразить это как

+--------+
|        |
| Vector |
|        |
|        |
+--------+
     |
     |
     v
+-------------------------------------------------+
|         |         |         |         |         |
| Element | Element | Element | Element | Element |
|         |         |         |         |         |
+-------------------------------------------------+

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

+--------+
|        |
| Vector |
|        |
|        |
+----+---+
     |
     |
     v
+----+----+---------+---------+
| Object  | Object  | Object  |
|  with   |  with   |  with   |
| Vector  | Vector  | Vector  |
+----+----+----+----+----+----+
     |         |         |   +---------+---------+---------+---------+---------+
     |         |         |   |         |         |         |         |         |
     |         |         +-->+ Element | Element | Element | Element | Element |
     |         |             |         |         |         |         |         |
     |         |             +-------------------------------------------------+
     |         |    +-------------------------------------------------+
     |         |    |         |         |         |         |         |
     |         +--->+ Element | Element | Element | Element | Element |
     |              |         |         |         |         |         |
     |              +-------------------------------------------------+
     |    +-------------------------------------------------+
     |    |         |         |         |         |         |
     +--->+ Element | Element | Element | Element | Element |
          |         |         |         |         |         |
          +---------+---------+---------+---------+---------+

Таким образом, все векторы находятся рядом друг с другом, но элементы, которые есть у векторов, могут находиться где угодно в памяти. По этой причине вы не хотите использовать std:vector<std::vector<int>> для матрицы. Все субвекторы получают память куда угодно, поэтому между строками нет места.


Обратите внимание, что это относится ко всем контейнерам с распределителем, поскольку они не хранят элементы внутри контейнера напрямую. Это не верно для std::array, так как, как и необработанный массив, элементы являются частью контейнера. Если у вас std::array<int, 20>, то его размер не менее sizeof(int) * 20.

7 голосов
/ 02 апреля 2019

размер

class Foo {

public:
    Foo() = default;
    Foo(std::vector<int> vec): _vec{vec} {}

private:
    std::vector<int> _vec;
};

известен и постоянен, внутренний std :: vector выполняет выделение в куче, поэтому нет никаких проблем с выполнением foovec.reserve(4);

иначе как std :: vector может быть в стеке? ; -)

3 голосов
/ 02 апреля 2019

Размер вашего класса Foo известен во время компиляции, класс std::vector имеет постоянный размер, поскольку элементы, которые он содержит, размещаются в куче.

std::vector<int> empty{};
std::vector<int> full{};
full.resize(1000000);
assert(sizeof(empty) == sizeof(full));

Оба экземпляраstd::vector<int>, empty и full всегда будут иметь одинаковый размер, несмотря на то, что они содержат разное количество элементов.

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

2 голосов
/ 02 апреля 2019

Когда вы создаете вектор типа T, а затем резервируете N элементов для этого вектора, компилятор в основном находит и резервирует непрерывный блок памяти

Компилятор не делает таких вещей.Он генерирует код для запроса хранения у распределителя вектора во время выполнения .По умолчанию это std::allocator, который делегирует operator new, который будет извлекать неинициализированное хранилище из системы времени выполнения.

Моя интеллектуальная модель ломается для пользовательских объектов неизвестных/ меняющийся размер

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

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


Ваш Foo завершен, и его размер фиксируется во время компиляции.Вы можете проверить это с помощью sizeof(Foo), sizeof(foovec[0]) и т. Д.

Вектор владеет переменным объемом памяти, но не содержит его вобъект.Он просто хранит указатель и зарезервированные и используемые размеры (или что-то эквивалентное).Например, экземпляр:

class toyvec {
  int *begin_;
  int *end_;
  size_t capacity_;
public:
  // push_back, begin, end, and all other methods
};

всегда имеет фиксированный размер sizeof(toyvec) = 2 * sizeof(int*) + sizeof(size_t) + maybe_some_padding.Выделение огромного блока памяти и установка begin в его начало оказывает нет влияния на размер самого указателя.


tl; dr C ++ не имеет динамически изменяемых объектов.Размер объекта постоянно фиксируется определением класса.C ++ имеет объекты, которые владеют и могут изменять размер динамического хранилища, но это не является частью самого объекта.

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