Перейти к содержанию

Последнее обновление:2026-10-12

Отказ от конструктора по умолчанию в C++

В C++ некоторые контейнеры при изменении размера методом resize() вызывают конструктор по умолчанию, что может быть вовсе не тем поведением, которое нам нужно. В этой статье рассказывается, как управлять этим процессом с помощью пользовательского аллокатора и избежать вызова конструктора по умолчанию.

Пример 1:

// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
vec.resize(5);
vec.resize(10);

Знаете, что будет выведено в этом случае?

Ответ:

[0, 1, 2, 3, 4]
[0, 1, 2, 3, 4, 0, 0, 0, 0, 0]

Здесь нужно понять несколько моментов:

Первое: почему после обратного resize данные превратились в нули, что произошло?

Второе: что делать, если я хочу сохранить исходные данные, то есть хочу получить такой вывод:

[0, 1, 2, 3, 4]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Третье: что произойдёт, если у элементов vector нет конструктора по умолчанию?

Четвёртое: какие ограничения есть у resize?

В этом разделе мы как раз займёмся этой мощной функциональностью — как заставить resize избежать вызова конструктора по умолчанию.

Примечание: из-за ограниченного объёма статьи весь код «ленивой» версии раздела обновлён в сообществе.

1. Разбор resize

Чтобы изложение было более плавным, я использую следующий пример: запретим конструирование Foo, а затем вызовем для него resize на vector.

class Foo {
 public:
  Foo() = delete;

 private:
  int a;
};

std::vector<Foo> f;
f.resize(10);

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

Первая проблема: в стеке вызовов видно, что мы дошли до __uninitialized_default_n, где проверяется, является ли текущий тип тривиальным, — это первое ограничение resize!

required from '_ForwardIterator std::__uninitialized_default_n(_ForwardIterator, _Size) [with _ForwardIterator = Foo*; _Size = long unsigned int]'
  710 |       return __uninitialized_default_n_1<__is_trivial(_ValueType)
      |              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  711 |                                          && __can_fill>::

Вторая проблема: поскольку мы запретили конструктор по умолчанию, аллокатору не удалось создать на исходном адресе через placement new объект по умолчанию, и возникла ошибка — это второе ограничение!

gcc/14.1.0_1/include/c++/14/bits/stl_construct.h:119:7: error: use of deleted function 'Foo::Foo()'
  119 |       ::new((void*)__p) _Tp(std::forward<_Args>(__args)...);
      |       ^~~~~~~~~~~~~~~~~~~

Раз мы уже дошли до этого, можно переходить к сути: видно, что при resize для выделения памяти используется аллокатор. Когда переданный размер больше size (предположим, что мы остаёмся в пределах capacity, то есть адрес валиден), аллокатор с помощью placement new создаёт объект, сконструированный конструктором по умолчанию.

Исходники реализации gcc можно показать здесь — просто, правда? Это всего лишь вызов construct.

__try
{
  typedef __gnu_cxx::__alloc_traits<_Allocator> __traits;
  for (; __n > 0; --__n, (void)++__cur)
    __traits::construct(__alloc, std::__addressof(*__cur));
  return __cur;
}
__catch(...)
{
  std::_Destroy(__first, __cur, __alloc);
  __throw_exception_again;
}

Итак, с принципом работы resize мы разобрались, вернёмся к вопросу из самого начала.

2. Как избежать вызова конструктора по умолчанию

Как же избежать вызова конструктора по умолчанию?

На самом деле всё просто: нужно отключить construct, используемый gcc по умолчанию. А как его отключить?

Здесь и пригодится второй параметр vector — аллокатор!

3. Аллокатор

Для решения этой проблемы можно использовать пользовательский аллокатор. Пользовательский аллокатор позволяет управлять поведением конструирования объектов и выбирать, использовать ли конструктор по умолчанию при вызове resize. Вот простая реализация пользовательского аллокатора — приведён только ключевой код.

template <class T>
class no_init_alloc {
 public:
  using value_type = T;
  // Override construct method to do nothing
  template <class U>
  void construct(U* p) {
    // Do nothing, memory is not initialized
  }
};

Нам достаточно, чтобы construct ничего не делал, — и тогда им можно пользоваться, то есть:

std::vector<Foo, no_init_alloc<Foo>> std_vec;

В результате получим:

[0, 1, 2, 3, 4]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

а не:

[0, 1, 2, 3, 4]
[0, 1, 2, 3, 4, 0, 0, 0, 0, 0]

Ну что, разобрались?

Конец раздела

Похожие темы

Комментарии