ХабрПостыНовостиМегапосты?

inplace_vector: новый контейнер C++26

Вы когда-нибудь мечтали о динамически расширяемом последовательном контейнере с фиксированной емкостью, хранящем свои элементы на стеке? Комитет по стандартизации 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++.

@eoanermine
10.02.2025 20:00 UTC
Первоисточник

Комментарии

@Vorono4ka
10.02.2025 21:22 UTC
0

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

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

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

11.02.2025 04:53 UTC
+2

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

11.02.2025 09:31 UTC
0

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

11.02.2025 12:36 UTC
+1

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

12.02.2025 04:14 UTC
0

Вектор имеет размер и емкость. Тут речь про статическкую емкость, а размер как раз может меняться. Для std::vector ближайшим аналогом будет, если вы изначальное сделаете reserve и при добавлении элементов сами будете следить чтобы size() не превышал capacity(). Но std::vector память выделит в куче.

@KivApple
12.02.2025 08:37 UTC
0

Лично мне не хватает гибридного варианта - вектора со статическим буфером на N элементов, но переходящий на динамическую аллокацию при превышении размера буфера.

Полезно, когда известно, что в 99% случаев алгоритм работает с небольшим числом элементов, но иногда может прилететь что-то большое.

Да, есть small object optimization, но он, во-первых, не гарантирован, во-вторых, им нельзя управлять (а оптимальное значение N может отличаться от алгоритма к алгоритму).

Такое есть в библиотеке Abseil и зовётся InlinedVector.

Аналогичная хотелка для std::any, который тоже имеет динамически выделяемый буфер и опять же во многих задачах можно прикинуть "большую часть времени нам приходят объекты размером не более N, где N очень специфичен для задачи".

Есть ли где-то такое, мне неизвестно.

12.02.2025 13:50 UTC
0

Такое есть в библиотеке Abseil и зовётся InlinedVector.

Есть также small_vector в boost, может когда-нибудь и его добавят в STL.

15.02.2025 01:35 UTC
+1

То что вы хотите, называется "small vector" pattern.

  • В LLVM есть SmallVector - именно оно самое.

  • Есть в Boost container::small_vector.

Можно наколхозить самому на std::pmr::vector из C++17, передав ему std::pmr::monotonic_buffer_resource или (un)synchronized_pool_resource, в конструктор конторого передать буфер на стеке. Как только закончится capacity, то такой вектор полезет аллоцировать память в куче.

@domix32
12.02.2025 19:15 UTC
0

Интересно из какого юзкейса возникла потребность в этом контейнере.

15.02.2025 12:52 UTC
-3

как обычно, потребность навалить побольше 💩 в стандарт, чтобы оттолкнуть еще больше старых пользователей C++

16.04.2026 10:34 UTC
0

Когда нельзя использовать кучу из-за плавающево времени выделения памяти.

@Fet1sh
13.02.2025 05:17 UTC
-2

Не так давно я написал в комитет WG21 с предложением добавить в std::vector новую функцию reset(). Она как бы должна была сбрасывать размер массива, чтобы на следующем кадре добавить элементы на те же места, типо как clear(), но сильно быстрее. Это было бы очень полезно если программа работает как ImGui, то есть в Immediate Mode, как было в моëм случае. Но очевидно меня погнали куда подальше с такими предложениями, потому что это вгоняет массив в неопределëнное состояние, что может быть критично.

Но я в целом не жалуюсь, в любом случае, эту функцию я взял из своего динамического массива FE2D::dynamic_array и там всё работает как надо 😉

16.04.2026 10:38 UTC
0

сбрасывать размер массива, чтобы на следующем кадре добавить элементы на те же места

Што это за жесть... :)

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

@Belarus
16.04.2026 10:40 UTC
0

А в чём разница между unchecked_push_back и push_back?