Вы когда-нибудь мечтали о динамически расширяемом последовательном контейнере с фиксированной емкостью, хранящем свои элементы на стеке? Комитет по стандартизации 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++.
Vorono4ka
Очень интересный пост! Спасибо большое за подробное описание применения этого контейнера, возможных ошибок и новых методов.
Пока что не могу придумать случаев, когда такой контейнер я бы мог использовать. Будто бы можно обойтись массивом и вручную считать необходимые индексы и размеры.
Разве что работа с итераторами упростится при использовании этого контейнера.
Notevil
Ну например если мы знаем что элементов может быть не больше чем N, но не знаем сколько именно, и динамические аллокации делать не хочется.
Mmv85
Почему бы тогда не использовать вектор размера N?
Yuuri
Вектор сделает динамическую аллокацию.