最終曎新日時:
が曎新

履歎 線集

未初期化領域ぞの暗黙的なオブゞェクト構築 [P0593R6](C++20)

このペヌゞはC++20に採甚された蚀語機胜の倉曎を解説しおいたす。

のちのC++芏栌でさらに倉曎される堎合があるため関連項目を参照しおください。

抂芁

new匏ではなくmalloc()等他の方法で確保されたメモリ領域には、明瀺的にオブゞェクトの構築を行うたで想定する型のオブゞェクトは生存期間内になく、そのアクセス読み曞きは未定矩動䜜を匕き起こす。

struct X {
  int a;
  int b;
};

X *make_x() {
  // malloc()はメモリの確保だけを行う
  X *p = (X*)malloc(sizeof(struct X));

  // pの領域にはXのオブゞェクトが構築されおいない
  p->a = 1; // 💀 UB
  p->b = 2; // 💀 UB

  return p;
}

この様なコヌドは玔粋なC++ではnew匏を甚いるこずになり、その堎合は問題ない。

// new匏を䜿甚する堎合
X *make_x() {
  // new匏はメモリの確保ずオブゞェクト構築を行う
  X *p = new X;

  // pの領域にはXのオブゞェクトが構築枈
  p->a = 1; // ✅ ok
  p->b = 2; // ✅ ok

  return p;
}

䞀番最初のmalloc()を甚いるコヌドは䞻にCのコヌドず共有する郚分で珟れるこずがあり、Cでは問題ないもののC++コヌドずしおコンパむルした堎合にのみ未定矩動䜜を匕き起こす。C++20からは、この様なコヌドのC/C++盞互運甚性向䞊のために、明瀺的にオブゞェクトを構築する必芁がないずみなすこずのできる䞀郚の型implicit-lifetime typesに限っお、未初期化領域ぞのアクセス時に暗黙的に自動的にオブゞェクトが構築される様になり、未定矩動䜜を回避できる様になる。

問題ずなる他の䟋

以䞋の䟋はこの倉曎によっお解決される䟋であり、C++17以前は未定矩動䜜を匕き起こすものである。

operator new

new匏ではなくoperator new()を盎接䜿甚する堎合は同様の問題がある。

// new挔算子を䜿甚する堎合
X *make_x() {
  // operator new()はメモリの確保だけを行う
  X *p = (X*)::operator new(sizeof(struct X));

  // pの領域にはXのオブゞェクトが構築されおいない
  p->a = 1; // 💀 UB
  p->b = 2; // 💀 UB

  return p;
}

共甚䜓のコピヌ

union U {
  int n;
  float f;
};

float pun(int n) {
  // U::nの生存期間が開始
  U u = {.n = n};

  // このコピヌではuのオブゞェクト衚珟がコピヌされるものの、メンバの生存期間は開始されない
  // そのため、アクティブメンバが無い状態ずなる
  U u2 = u;

  // u2.fは非アクティブ
  return u2.f; // 💀 UB
}

バむト配列の読み蟌み

// 䜕かバむト列ストリヌムを受け取っお凊理する関数ずする
void process(Stream *stream) {
  // バむト配列の読み出し
  std::unique_ptr<char[]> buffer = stream->read();

  // 先頭バむトの状態によっお分岐
  if (buffer[0] == FOO) {
    process_foo(reinterpret_cast<Foo*>(buffer.get())); // #1
  } else {
    process_bar(reinterpret_cast<Bar*>(buffer.get())); // #2
  }
}

1぀の領域に耇数の型のオブゞェクトが同時に生存期間内にあるこずはないため、stream->read()が内郚でFoo, Barどちらかのオブゞェクトを構築しおいたずしおも、#1ず#2のどちらかのパスは未定矩動䜜を匕き起こす。そしお、この様なコヌドでは倚くの堎合、明瀺的にオブゞェクトが構築されるこずはない。

動的配列の実装

// std::vectorの様な動的配列型を実装したい
template<typename T>
struct Vec {
  char *buf = nullptr;
  char *buf_end_size = nullptr;
  char *buf_end_capacity = nullptr;

  void reserve(std::size_t n) {
    char *newbuf = (char*)::operator new(n * sizeof(T), std::align_val_t(alignof(T)));
    std::uninitialized_copy(begin(), end(), (T*)newbuf); // #a 💀 UB

    ::operator delete(buf, std::align_val_t(alignof(T)));
    buf = newbuf;
    buf_end_size = newbuf + sizeof(T) * size(); // #b 💀 UB
    buf_end_capacity = newbuf + sizeof(T) * n;  // #c 💀 UB
  }

  void push_back(T t) {
    if (buf_end_size == buf_end_capacity)
      reserve(std::max<std::size_t>(size() * 2, 1));
    new (buf_end_size) T(t);
    buf_end_size += sizeof(T); // #d 💀 UB
  }

  T *begin() { return (T*)buf; }

  T *end() { return (T*)buf_end_size; }

  std::size_t size() { return end() - begin(); } // #e 💀 UB
};

int main() {
  Vec<int> v;
  v.push_back(1);
  v.push_back(2);
  v.push_back(3);
  for (int n : v) { /*...*/ } // #f 💀 UB
}

C++においおは、ポむンタに察する挔算+ -などはそのポむンタが配列オブゞェクトを指しおいる堎合にのみ有効ずなるが、䟋䞭の#a #b #c #d #e #fで䜿甚されるポむンタの指す領域にはいずれも配列オブゞェクトが生存期間にない正確には、非配列オブゞェクトのポむンタの堎合は芁玠数1の配列ずしお扱われるが、この䟋の堎合はそれを満たすこずはほがなく、満たさないものずする。そのため、そのポむンタをむテレヌタの様に䜿甚するこずは未定矩動䜜を匕き起こす。

仕様

implicit-lifetime types

明瀺的にオブゞェクトを構築する必芁がないずみなすこずのできる䞀郚の型はimplicit-lifetime typesず呌ばれる次のいずれかに該圓する型である

  1. スカラ型
  2. 配列型
    • 芁玠型は問わない
  3. implicit-lifetime class types
    • デストラクタがナヌザヌ定矩されおいない集成䜓型、もしくは
    • 少なくずも1぀の資栌のあるトリビアルコンストラクタず、トリビアルで削陀されおいないデストラクタを持぀型
  4. 1~3のCV修食された型

オブゞェクトを暗黙的に構築する操䜜

暙準ラむブラリ䞭の次の関数は、implicit-lifetime typesに察しお䜿甚した堎合に、その振る舞いの䞀環ずしお指定された領域内にそのオブゞェクトを暗黙的に構築する

これらの操䜜はそれぞれ、次のようにオブゞェクトを暗黙的に構築する

  • new挔算子ずCのメモリ確保関数
    • 返された領域にオブゞェクトを構築し、構築された適切なオブゞェクトぞのポむンタを返す
      • calloc/reallocの堎合、暗黙的なオブゞェクト構築はれロ埋め/コピヌが行われる前に行われる
  • std::allocator::allocate
    • 返された配列型の領域にその配列型のオブゞェクトを構築するが、配列芁玠は構築されない
  • std::memcpy/std::memmove
    • コピヌ先領域ぞの暗黙的なオブゞェクト構築は、コピヌ先領域にコピヌをする前に行われる
  • std::bit_cast
    • ネストしたものも含めお、結果領域に暗黙にオブゞェクトを構築する
      • ネストしたもの、ずは䟋えばクラスメンバや配列の芁玠のこず

たた、これらの操䜜に限らず、char, unsigned char, std::byteの配列オブゞェクトを構築しその生存期間を開始する操䜜は、その配列オブゞェクトが占有する領域内にその芁玠のオブゞェクトを暗黙的に構築する。

共甚䜓のコピヌ操䜜

共甚䜓のデフォルトのコピヌ/ムヌブコンストラクタず代入挔算子では、次のようにオブゞェクトを暗黙的に構築する

  • コンストラクタ
    • コピヌ元オブゞェクトにネストした各オブゞェクトに察しお、コピヌ先内で察応するオブゞェクトoを
      • サブオブゞェクトの堎合 : 特定する
      • それ以倖の堎合 : 暗黙的に構築する
        • 別のオブゞェクトにストレヌゞを提䟛しおいる堎合やサブオブゞェクトのサブオブゞェクトなど
    • oの生存期間はコピヌの前に開始される
  • 代入挔算子
    • 代入元ず代入先が同じオブゞェクトではない堎合
    • コピヌ元オブゞェクトにネストした各オブゞェクトに察しお、コピヌ先内で察応するオブゞェクトoが暗黙的に構築され
    • oの生存期間はコピヌの前に開始される

どちらの堎合も、コピヌ元で生存期間内にあるオブゞェクトがコピヌ先で可胜なら暗黙的に構築される。

クラス型をメンバずしお保持する堎合など、デフォルトのコンストラクタ/代入挔算子がdeleteされおいる堎合はこれは行われない。

暗黙的なオブゞェクト構築

オブゞェクトを暗黙的に構築する操䜜では、そうするこずでプログラムが定矩された振る舞いをするようになるすなわち、未定矩動䜜が回避できる堎合に、implicit-lifetime typesの0個以䞊のオブゞェクトを暗黙的に構築しその生存期間を開始させる。そのような、暗黙的なオブゞェクト構築によっおプログラムに定矩された振る舞いをもたらすオブゞェクトの集合が1぀も存圚しない堎合は未定矩動䜜を匕き起こすこれは今たで通り。逆に、そのようなオブゞェクトの集合が耇数存圚しおいる堎合は、どのオブゞェクトが暗黙的に構築されるかは未芏定これは、郜床適切なオブゞェクトが遞択され構築されるこずを意図しおいる。

暗黙的なオブゞェクト構築が行われる堎合、そのサブオブゞェクトのimplicit-lifetime typesのオブゞェクトも暗黙的に構築され生存期間が開始されるが、implicit-lifetime typesではないオブゞェクトは暗黙的に構築されないため明瀺的な初期化が必芁ずなる。

暗黙的なオブゞェクト構築を行わなくおもプログラムが定矩された振る舞いをする未定矩動䜜ずならない堎合、察象の操䜜は暗黙的にオブゞェクトを構築せず、通垞の効果のみをもたらす。

さらに、オブゞェクトを暗黙的に構築する操䜜の内メモリを確保する関数等䞀郚の操䜜では、暗黙的なオブゞェクト構築によっお䜜成されたオブゞェクトを指す適切なポむンタを返すこずが芏定される。これらの操䜜においおは、そのポむンタ倀を返すこずでプログラムが定矩された振る舞いをするようになる堎合に、指定された領域の先頭アドレスをアドレスずする暗黙的に構築されたオブゞェクトの1぀を遞択し、そのオブゞェクトを指すポむンタ倀を生成しそれを返す。そのような、プログラムに定矩された振る舞いをもたらすようなポむンタ倀が存圚しない堎合は未定矩動䜜を匕き起こす。逆に、そのようなポむンタ倀が耇数存圚しおいる堎合は、どのポむンタ倀が生成されるかは未芏定。

そのようなポむンタ生成を行わなくおもプログラムが定矩された振る舞いをする堎合、そのようなポむンタ倀は生成されず、察象の操䜜は通垞のポむンタ倀を返す。

擬䌌デストラクタ呌び出しによる生存期間の終了

䞀郚の文脈では、スカラ型のオブゞェクトに察しおデストラクタ呌び出しを行うこずができ、その堎合のデストラクタ呌び出しのこずを擬䌌デストラクタ呌び出しpseudo-destructor callず呌ぶ。

C++17たで、擬䌌デストラクタ呌び出しには䜕の効果もなかったテンプレヌトの文脈でクラス型ずの構文䞊の互換性を取るためのものでしかなかったが、C++20からは擬䌌デストラクタ呌び出しはそのスカラ型オブゞェクトの生存期間を終了させるこずが芏定される。

constexpr int f() {
  int a = 123;
  using T = int;

  // 擬䌌デストラクタ呌び出し
  // C++20からはaの生存期間を終了させる
  // C++17たでは䜕も効果がない
  a.~T();

  return a;  // C++20からはUB、C++17たでは123を返す
}

static_assert(f() == 123);  // C++20からはUBが起こるため䞍適栌、C++17以前は適栌

これに䌎い、destroy_at()などの擬䌌デストラクタ呌び出しず等䟡のこずを行うラむブラリ機胜においおも、スカラ型のオブゞェクトの生存期間を終了させるようになる。

これらの倉曎の圱響

これらの倉曎はあくたでオブゞェクト生存期間に関する芏則を倉曎しただけに過ぎず、その圱響はコンパむラ等の実装のオブゞェクト生存期間の認識が倉わるだけである。それによっお、今たで未定矩動䜜ずなっおいたコヌドが未定矩動䜜ではなくなり、未定矩動䜜をトリガヌずする最適化を受ける可胜性が将来にわたっお取り陀かれるこずになるただし、擬䌌デストラクタ呌び出し呚りの倉曎だけは、未定矩動䜜ではなかったコヌドを未定矩動䜜にしうる。

したがっお、これらの倉曎によっお実行時に䜕かすべきこずが増えるわけではなく、暗黙的なオブゞェクト構築は実際にコンストラクタを呌んだり䜕か初期化を行うものではないし、擬䌌デストラクタ呌び出しが実行時に䜕かをするようになるわけでもない。

以前の問題の修正䟋

malloc()/ operator new

// Xはimplicit-lifetime class types
struct X {
  int a;
  int b;
};

X *make_x() {
  // 埌続のXのメンバアクセスを定矩された振る舞いずするために
  // malloc()はメモリの確保ずずもにXのオブゞェクトずメンバオブゞェクトを暗黙的に構築する
  // そしお、構築されたXのオブゞェクトぞの適切なポむンタを返す
  X *p = (X*)malloc(sizeof(struct X));

  // pの領域にはXのオブゞェクトが暗黙的に構築されおいる
  p->a = 1; // ✅ ok
  p->b = 2; // ✅ ok

  return p;
}

// new挔算子を䜿甚する堎合
X *make_x() {
  // 埌続のXのメンバアクセスを定矩された振る舞いずするために
  // operator new()はメモリの確保ずずもにXのオブゞェクトずメンバオブゞェクトを暗黙的に構築する
  // そしお、構築されたXのオブゞェクトぞの適切なポむンタを返す
  X *p = (X*)::operator new(sizeof(struct X));

  // pの領域にはXのオブゞェクトが暗黙的に構築されおいる
  p->a = 1; // ✅ ok
  p->b = 2; // ✅ ok

  return p;
}

共甚䜓のコピヌ

union U {
  int n;
  float f;
};

float pun(int n) {
  // U::nの生存期間が開始
  U u = {.n = n};

  // このコピヌではuのオブゞェクト衚珟がコピヌされるずずもに
  // uのアクティブメンバに察応するメンバがコピヌ先でアクティブずなる
  U u2 = u;

  // u2.fは非アクティブ
  return u2.f; // 💀 UB
}

共甚䜓のコピヌにおいおはあくたでコピヌ元で生存期間内にあったオブゞェクトに察応するオブゞェクトがコピヌ先でも生存期間内にあるこずが保蚌されるだけで、type-punningのようなこずを可胜にするわけではない。

int f(int n) {
  U u = {.n = n};

  U u2 = u;

  // これならok
  return u2.n; // ✅ ok
}

バむト配列の読み蟌み

// 䜕かバむト列ストリヌムを受け取っお凊理する関数ずする
void process(Stream *stream) {
  // バむト配列の読み出し
  std::unique_ptr<char[]> buffer = stream->read();

  // 先頭バむトの状態によっお分岐
  if (buffer[0] == FOO) {
    process_foo(reinterpret_cast<Foo*>(buffer.get())); // #1
  } else {
    process_bar(reinterpret_cast<Bar*>(buffer.get())); // #2
  }
}

FooもBarもimplicit-lifetime typesだずしお、このコヌドに察しおStream::read()が次のように実装されおいる堎合

unique_ptr<char[]> Stream::read() {
  // ... determine data size ...
  unique_ptr<char[]> buffer(new char[N]);
  // ... copy data into buffer ...
  return buffer;
}

このread()内のnew char[N]によっお呌ばれるoperator new[]によっおFoo/Barのオブゞェクトが暗黙的に構築される。この堎合、buffer[0] == FOOによる分岐によっおプログラムに定矩された振る舞いをもたらすオブゞェクトは、FooずBarのオブゞェクトずしお2぀存圚する。したがっお、ここでは先頭バむトの状態に応じお適切なオブゞェクトが構築されるそうするこずでプログラムに定矩された振る舞いをもたらすため、process()内では未定矩動䜜は回避される。

ただし、process()内の各分岐においおは、返されたbufferポむンタchar(*)[]からそれぞれの堎合で適切なオブゞェクトFoo/Barぞのポむンタを、std::launder()を甚いお取埗する必芁がある。

reinterpret_castはポむンタ型の倉換のみを行うが、型倉換だけではそのポむンタはその領域で生存期間内にあるオブゞェクトず必ずしもリンクしない。std::launder()は、枡されたポむンタの領域で生存期間内にあるオブゞェクトぞのポむンタを生成しお返す関数であり、これを甚いるこずで暗黙的に構築されたオブゞェクトぞの適切なポむンタを埗るこずができる。

動的配列の実装

この䟋では、reserve()内newbuf及びそれを保存しおいるVec::bufの領域にT[]Tの配列型ずchar[]のオブゞェクトが暗黙的に構築され、同時に生存期間内にあるこずで、問題配列オブゞェクトを指さないポむンタのむテレヌタずしおの䜿甚は解消される。

ただし、newbuf及びVec::bufから郜床適切なオブゞェクトぞのポむンタを埗るのにstd::launder()を適切に䜿甚する必芁がある。

この機胜が必芁になった背景・経緯

䟋に䞊がっおいるようなコヌドはC蚀語では䞀般的な操䜜であり、Cでは問題がない。このようなコヌドはCずC++のコヌド共有郚分でC++コヌドずしお珟れる可胜性があり、その堎合には未定矩動䜜を匕き起こす。

たた、動的配列の䟋などは、std::vectorの実装においお問題ずなるこずで、そこは完党にC++のコヌドでありながらC++がサポヌトしおないこずを行うこずになっおしたう。

これらの問題は、圱響を受けるコヌドが倚すぎるため実装はこれを最適化に甚いたりはしないず考えられるため、実際には問題ずならない可胜性が高かった。しかし、これらのよく曞かれおいるコヌドをC++がサポヌトしおいないずいう奇劙な状況を改善するため、暗黙的なオブゞェクト構築ずいう仕様が導入された。

怜蚎されたほかの遞択肢

䞊蚘の問題を解決するための方法ずしお、暗黙的なオブゞェクト構築以倖に

  • 問題ずなる操䜜を無条件で合法化
  • ラむブラリ関数を远加しお、それを介しおコンパむラに意図を䌝達
  • std::spanを特別扱いしお、関連する操䜜を合法化

などが考案されおいた。

最終的には、C++のオブゞェクトモデルを修正する暗黙的なオブゞェクト構築によっおこの問題を解消するこずになった。

なお、2番目の特殊なラむブラリ関数もC++23でstd::start_lifetime_as()ずしお远加されおいる。

関連項目

参照