Вы когда-нибудь мечтали о динамически расширяемом последовательном контейнере с фиксированной емкостью, хранящем свои элементы на стеке? Комитет по стандартизации C++ исполняет желания! Теперь вам не нужно обращаться к Boost.Container за boost::container::static_vector. Встречайте, std::inplace_vector (P0843), принятый в C++26!

#include <inplace_vector>

int main() {
  // Он почти как обычный std::vector, но ёмкость std::vector может динамически изменяться
  // В случае же с std::inplace_vector ёмкость задается статически как шаблонный параметр
  // и не может изменяться
  std::inplace_vector<int, /* capacity = */ 5> inplace_vec;

  // Все операции, которые поддерживает std::vector — их поддерживает и std::inplace_vector
  inplace_vec.push_back(0);
}

Прежде чем перейти к рассмотрению нюансов работы с ним, ответим на главный вопрос: зачем вообще в стандартной библиотеке нужен ещё один контейнер?

Авторы P0843 отвечают на этот вопрос так: std::inplace_vector будет вам полезен, если вы хотите разместить в статической памяти объекты, но при этом по каким-то причинам не можете воспользоваться std::array. Например, если ваши объекты не default constructible:

#include <array>
#include <inplace_vector>
#include <vector>

struct S {
  S() = delete;
  S(int) { }
};

int main() {
  // Код ниже не скомпилируется, так как std::array конструирует все свои объекты при инициализации
  // std::array<S, 5> arr;

  // Код ниже скомпилируется, но что, если мы не хотим (или не можем) динамически выделять память?
  std::vector<S> vec;

  // Код ниже компилируется и хранит элементы на стеке
  std::inplace_vector<S, /* capacity = */ 5> inplace_vec;
  inplace_vec.emplace_back(10); // Тут не будет никаких динамических аллокаций
}

Если вдруг мы попробуем положить в std::inplace_vector больше элементов, чем позволяет его ёмкость, мы получим std::bad_alloc:

#include <iostream>
#include <inplace_vector>

int main() {
  std::inplace_vector<int, 5> inplace_vec;

  for (int i = 0; i != 5; ++i) {
    inplace_vec.emplace_back(i);
  }

  try {
    inplace_vec.emplace_back(5);
    assert(false); // Исполнение никогда не дойдет до этого assert
  } catch (const std::bad_alloc&) {
    std::cout << "Привет, bad_alloc!" << std::endl;
  }
}

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

  1. После того, как вы переместили контейнер, его размер может измениться. А может и не измениться. Кроме того, асимптотическая сложность перемещения (так же как std::swap) std::inplace_vector хуже, чем перемещения std::vector: O(size) против O(1).

#include <inplace_vector>

int main() {
  std::inplace_vector<T, 10> a(10);
  std::inplace_vector<T, 10> b(std::move(a));
  assert(a.size() == 10); // MAY FAIL
}

Если говорить конкретно, размер std::inplace_vector не будет изменен после перемещения, если тип T тривиально-копируемый (то есть, assert в коде выше выполнится успешно).

Но если тип T не тривиально-копируемый, то перемещенный контейнер будет оставлен в «корректном, но неспецифицированном состоянии» (то есть, в таком случае assert в коде может и не выполниться).

  1. В отличие от std::vector, перемещение std::inplace_vector инвалидирует все итераторы. std::swap двух контейнеров также инвалидирует все итераторы обоих контейнеров.

#include <inplace_vector>
#include <vector>

int main() {
  {
    std::vector<int> a(10);
    auto it = a.begin();
  
    std::vector<int> b(std::move(a));

    // Согласно гарантиям std::vector, мы всё еще можем использовать it
    // После перемещения итераторы остаются действительны
    std::cout << *it;
  }

  {
    std::inplace_vector<int, 10> a(10);
    auto it = a.begin();
  
    std::inplace_vector<int, 10> b(std::move(a));
    // Если вместо строки выше написать:
    // std::vector<int> b;
    // std::swap(a, b);
    // То результат будет тот же
  
    // В отличие от std::vector, после перемещения std::inplace_vector
    // все итераторы на его элементы инвалидируются
    std::cout << *it; // Скомпилируется, но UB
  }
}
  1. В отличие от std::vector<T>, std::inplace_vector<T, N> является тривиально копируемым, если тип T является тривиально копируемым и N != 0 (std::vector ни в каких случаях не является тривиально копируемым).

#include <cstring>
#include <inplace_vector>
#include <vector>

int main() {
  // Так делать нельзя, это приведёт к двойному освобождению памяти
  {
    std::vector<int> a = {1, 2, 3, 4, 5};
    std::vector<int> b(5);
    std::memcpy(static_cast<void*>(&b), static_cast<void*>(&a), sizeof(a));
  }

  // А так делать можно: все элементы ведь хранятся на стеке
  {
    std::inplace_vector<int, 5> a = {1, 2, 3, 4, 5};
    std::inplace_vector<int, 5> b;
    std::memcpy(static_cast<void*>(&b), static_cast<void*>(&a), sizeof(a));
  }
}

Кроме того, std::inplace_vector обладает собственным набором уникальных методов для случаев, когда мы хотим вставить в контейнер новые элементы, не рискуя получить std::bad_alloc — вместо того, чтобы выбрасывать исключение, при превышении емкости они возвращают nullptr (единственное исключение —try_append_range, возвращающий итератор на первый элемент, который не удалось вставить):

constexpr T* inplace_vector<T, C>::try_push_back(const T& value);
constexpr T* inplace_vector<T, C>::try_push_back(T&& value); 

template<class... Args>
  constexpr T* try_emplace_back(Args&&... args);

template<container-compatible-range<T> R>
  constexpr ranges::iterator_t<R> try_append_range(R&& rg);
#include <inplace_vector>

int main() {
  std::inplace_vector<int, 5> inplace_vec;

  if (!inplace_vec.try_push_back(10)) {
    std::cerr << "Не получилось вставить элемент" << std::endl;
    std::terminate();
  }
  // inplace_vec = {10}

  auto init_list = {1, 2, 3, 4, 5};
  if (auto it = inplace_vec.try_append_range(init_list); it != init_list.end()) {
    // inplace_vec = {10, 1, 2, 3, 4}
    std::cerr << "Не получилось вставить элементы: ";
    for (; it != init_list.end()) {
      std::cerr << *it << ' ';
    }
    std::cerr << std::endl;
  }

  // При выполнении будет выведено следующее сообщение:
  // Не получилось вставить элементы: 5
}

И, конечно, для рисковых людей существует еще один набор методов: они не возвратят вам nullptr, если что-то пойдет не так. Это просто будет UB:

constexpr T& inplace_vector<T, C>::unchecked_push_back(const T& value);
constexpr T& inplace_vector<T, C>::unchecked_push_back(T&& value);

template<class... Args>
  constexpr T& unchecked_emplace_back(Args&&... args);
#include <inplace_vector>

int main() {
  std::inplace_vector<int, 5> inplace_vec;

  for (int i = 0; i != 5; ++i) {
    // Всё хорошо, так как мы еще не исчерпали емкость
    inplace_vec.unchecked_emplace_back(i);
  }

  // А тут мы её уже исчерпали. Код скомпилируется, но у нас UB
  inplace_vec.unchecked_emplace_back(5);
}

Наконец, внимательный читатель спросит: а где можно пощупать и поиграться с этим std::inplace_vector? К сожалению, по состоянию на 8 февраля 2025 г. он не реализован ни в libstdc++, ни в libc++, ни в MSVC STL. Так что boost::container::static_vector, которым вдохновлялись авторы std::inplace_vector, всё еще остается актуален.

Кроме того, существует несколько header-only реализаций std::inplace_vector: bemanproject/inplace_vector, Quuxplusone/SG14. Если вам нетерпится опробовать новый контейнер, вы можете взять любую из них, только будьте осторожны: их корректность вряд-ли проверялась так тщательно, как проверяются патчи в libstdc++ и libc++.

Комментарии (4)


  1. Vorono4ka
    10.02.2025 21:22

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

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

    Разве что работа с итераторами упростится при использовании этого контейнера.


    1. Notevil
      10.02.2025 21:22

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


      1. Mmv85
        10.02.2025 21:22

        Почему бы тогда не использовать вектор размера N?


        1. Yuuri
          10.02.2025 21:22

          Вектор сделает динамическую аллокацию.