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