コンテンツにスキップ

最終更新: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 になったのか、何が起きたのでしょうか?

第二に:もし元のデータを残したい場合は、どうすればよいでしょうか?つまり、出力結果が次のようになってほしい場合:

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

第三に:vector のメンバがデフォルトコンストラクタを持たない場合、何が起きるでしょうか?

第四に:resize にはどのような制限がありますか?

そこで本節では、この強力な機能を研究し、resize がデフォルト構築を呼び出さないようにする方法を見ていきます。

注:紙幅の都合上、省略版の全コードは星球で更新しています。

1.resizeの解析

本記事をよりスムーズに進めるため、次の例を使用します。Foo の構築を禁止し、その上で vector に対して resize を行います。

class Foo {
 public:
  Foo() = delete;

 private:
  int a;
};

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

このときコンパイルエラーになり、大量のエラーメッセージが表示されます。

一つ目の問題:呼び出しスタックが __uninitialized_default_n に到達しているのが分かります。ここで現在の型が trivial 型かどうかをチェックしており、これが 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.デフォルト構築の呼び出しを回避する

デフォルト構築の呼び出しを回避するにはどうすればよいでしょうか?

実はとても簡単で、gcc デフォルトの construct を無効化すればよいのです。どうやって無効化するのでしょうか?

ここで活躍するのが、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]

さて、理解できましたか?

本節はこれで終わりです

関連トピック

コメント