C++ のムーブを理解する

2022/07/21に公開

はじめに

本記事では C++ のムーブの解説を試みます。

C++ プログラマーにとって、ムーブは今では必修科目となっているにもかかわらず、いざ勉強しようとするとムーブ・セマンティクスだとか右辺値参照だとか所有権だとか、初めて見る用語のオンパレードで怖気づいてしまう方も少なくないのではないかと想像します。 私もそうでした。
この記事では、まずはそういった難しいことは脇に置いておいて、考え方をざっと理解いただくことを目指しました。

過去 10 年間以上にわたって多くの方々が取り組まれたテーマで今さらという感じがしますが、さまざまな解説記事があってもよいでしょう。

記事の構成

本記事は次の部分から構成されます。

  • ムーブとは何か?
  • ムーブの目的
  • ムーブの原理
  • ムーブの構成要素
  • ムーブの応用

ムーブとは何か?

ムーブとはオブジェクトを移動すること

C++ におけるムーブ (move) は、コピー (copy; 複製する) に対する用語で、意味はそのまま「移動」です。
移動の対象はオブジェクトで、オブジェクトを移動することをムーブと呼びます。

突然ですが、ここで PC のファイル操作を考えてみます。
ファイルを元の場所から別のフォルダー (ディレクトリ) にコピーすると、元のファイルはそのままに、内容がまったく同じファイルがもうひとつ作られます。 全体で見るとファイルの数は 1 個から 2 個に増えます。
それに対して、ファイルを移動すると元のフォルダー (ディレクトリ) からはファイルはなくなります。 ファイルの数は 1 個のまま変わりません。

C++ におけるムーブもこれと同じで、オブジェクトを複製するのではなく、元の場所から別の場所に移動することを意味します。
場所というのはメモリ上の位置 (アドレス) のことです。

知らず知らずのうちにコピーしている

先ほど、ムーブはコピーに対する用語であると述べました。
とは言っても、そもそもコピー自体になじみがないという方もいらっしゃるのではないでしょうか。

実は、私たちは知らず知らずのうちにオブジェクトをコピーしているのです。
次の code 1 をご覧ください。

code 1
  int a = 100;
  int b = a;

このコードには int 型の変数 a, b が登場します。
実行後の変数 b の値は変数 a と同様に 100 となっていることはおわかりだと思います。
これは変数 a の値が変数 b にコピーされているためです。

続いて code 2 をご覧ください。

code 2
  int a = 100;
  int b = 0;
  b = a;

こちらのコードも、実行後の変数 b の値は変数 a と同様に 100 となっていることでしょう。
こちらも同様に変数 a の値が変数 b にコピーされているためです。

このようなコピーは、ほかにも関数に引数を渡すときや、関数から値を返すときにも知らず知らずのうちに行われています。

デフォルトはコピー

先ほどご覧いただいたコード例のように、特に意識しないとオブジェクトはコピーされます。
言い換えると、 C++ ではデフォルトの動作はコピーであるということです。
これは C と同じです。

この理由は、よく言われるように C はコンピュータの CPU やメモリと対応するように言語仕様が作られていることと、 CPU が提供している動作がコピーだったからではないかと推測しています。

ムーブの目的

ムーブの最大の目的は効率化です。
C++ 標準化委員会への提案文書 N1377[3] でも、 Motivation の節に次のように書かれています:

Move semantics is mostly about performance optimization

次の 2 つの条件が成立している場合に、ムーブを使うことでコピーよりも効率が高まります。

  1. コピーする必要がなく、ムーブでもかまわない。
  2. ムーブの方がコピーよりも速い。

なお、ムーブの速度がコピーと変わらないことはありますが、ムーブの方がコピーよりも遅くなることは通常はありません。
そのため、条件 2 は基本的に考慮する必要はなく、条件 1 のみでムーブするのかコピーするのか判断すればだいじょうぶです。

実際には、効率化以外の目的でムーブを使用することもあります。
ただ、最初からいろいろ考えると混乱してしまうので、まずは効率化が目的だと捉えてください。
それ以外の用途は最後にご紹介したいと思います。

ムーブの原理

まずは figure 1 をご覧ください。
Data クラスと SubData クラスとの間にコンポジションや集約の関連があります。

figure 1

両クラス間の関連の実現方法はいくつかありますが、 C++ の場合はポインタとする方法があります。
次の code 3 はその例です。

code 3
class SubData;

class Data {
  SubData *subData_;
};

コピー

この Data クラスのインスタンスをコピーすることを考えます。
単純に subData_ データメンバをコピーすると、これはポインタなので figure 2 のようになってしまいますね。

figure 2

本来やりたいのは figure 3 ですね。

figure 3

このような結果になるようにするには、 Data クラスのインスタンスのコピー時に SubData クラスのインスタンスもコピーする必要があります。

ご参考までに、 figure 3 のようなディープコピーを実現する Data クラスの実装例です。
code 4
class Data {
 public:
  // default constructor
  Data()
      : subData_(new SubData()) {
  }

  // copy constructor
  Data(const Data &src)
      : subData_(new SubData(*src.subData_)) {
  }

  // destructor
  ~Data() {
    delete subData_;
  }

  // copy assignment operator
  Data& operator=(const Data &src) {
    if (&src == this) {
      return *this;
    }

    SubData *tmpSubData = new SubData(*src.subData_);
    delete subData_;
    subData_ = tmpSubData;
    return *this;
  }

 private:
  SubData *subData_;
};

実際のプログラムでは subData に相当するオブジェクトは 1 個だけではないでしょうし、 subData がさらに別のオブジェクト (言ってみれば subSubData) を持っていることも多いでしょう (figure 4)。

figure 4

こういったことを考えると、オブジェクトのコピー (ディープコピー) はコスト (時間) がかかるということがおわかりいただけると思います。
もちろん、 int など組み込み型のオブジェクトならたいしたコストではないのですが。

ムーブ

ようやくムーブです。 お待たせしました。
先ほどの例において、 figure 5 のような結果となる操作を考えます。

figure 5

subData1 をコピーするのではなく、 data1 が持っていた subData1 をそのまま data2 に移します。
もしこのような操作が可能であれば、 subData1 オブジェクトのコピーにかかるコストを省略できることになります。

まさにこのような操作がムーブであり、ムーブがコピーよりも効率的 (高速) である理由です。

figure 5 のようなムーブを実現する Data クラスの実装例です。

コピーコンストラクタでも、コピー代入演算子でも、 src.subData_ の値 (アドレス) を自分自身の subData_ にコピーしているだけです。
new SubData() していないところがポイントですね。

なお、そのあとで src.subData_ = nullptr; しているのは、デストラクタでの delete subData_; が 2 回実行されてしまためです。
また、これのためにコピーコンストラクタやコピー代入演算子の引数の const を消しています。

code 5
class Data {
 public:
  // copy constructor
  Data(/* const */ Data &src)
      : subData_(src.subData_) {
    src.subData_ = nullptr;
  }

  // copy assignment operator
  Data& operator=(/* const */ Data &src) {
    if (&src == this) {
      return *this;
    }

    delete subData_;
    subData_ = src.subData_;
    src.subData_ = nullptr;
    return *this;
  }

 private:
  SubData *subData_;
};

ムーブの出番

ここまでの解説でコピーに対するムーブの効率の高さは理解いただけたと思います。
ただ、これはご説明に用いた Data - SubData のような構造になっている場合にのみ適用可能な手法です。
そのようなケース、つまりムーブの出番がどのくらいあるのか疑問に感じる方もいらっしゃるかもしれません。

定量的なことは言えませんが、筆者は「多い」と考えています。
実行時に数が変わる (増減する) であるとか、実行時にならないと内容が確定せずインスタンス化できないといったようなことはよくあると思います。
このような場合には必然的に先の Data - SubData のような構造になるのではないでしょうか。

あるいは、実行時に増減するようなデータを扱う際には std::vector のようなコンテナを使うことも多いと思います。
コンテナも、まさにムーブが効果を発揮するデータ構造になっています。
試しに code 6 を Wandbox (コンパイラは gcc 12.1.0) で実行してみた結果です[6]

code 6
#include <iostream>
#include <vector>

int main() {
  std::vector<int> iv;
  std::cout << sizeof(iv) << '\n';

  iv.reserve(1'000);
  for (int i = 0; i < 1'000; ++i) {
    iv.push_back(i);
  }
  std::cout << sizeof(iv) << '\n';
}
24
24

std::vector クラステンプレート (の libstdc++ による実装) では、要素数が 0 でも 1,000 でも、 std::vector<int> 型のオブジェクト iv 自身のサイズは 24 で変わりません[7]
push_back された要素が実際に格納される先は iv オブジェクト内ではなく、これとは別に確保された領域です。
iv オブジェクトは、その別に確保された領域へのポインタを持っているはずです。

4 Bytes の int 型オブジェクトを 1,000 個 push_back しているので、少なくとも 4,000 Bytes が確保されているはずです。
コピーする場合には 24 + 4,000 Bytes のコピーが必要ですが、ムーブなら 24 Bytes ですみます。

標準ライブラリのコンテナは、 std::array を除いて (つまり、アロケータを受け取るものはすべて) このような構造になっていますので、ムーブの効果が発揮されます。

ムーブが可能な根拠

ここでひとつ疑問が生じるのではないでしょうか。
figure 1 のクラス図によれば、 Data クラスのオブジェクトは SubData クラスのオブジェクトを 1 個必ず持つことになっています。

figure 1 (再掲)

しかし、 figure 5 の data1 オブジェクトは、もともと持っていた subData1 オブジェクトを data2 オブジェクトに奪われてしまっています。

figure 5 (再掲)

そのため、 data1 オブジェクトはもはや正当な Data クラスのインスタンスとして振る舞うことができなくなっています。
たとえば、 subData_ データメンバを経由して SubData クラスのオブジェクトにアクセスするようなメンバ関数は「正しく」動作する保証がありませんね。

しかし、ムーブの目的の条件 1 「コピーする必要がなく、ムーブでもかまわない。」を思い出してください。
ムーブとはオブジェクトを移動することで提示した PC のファイル操作のたとえでもご説明したようにムーブすると元の場所からはなくなってしまうわけですから、この条件 1 は「それ以降はそのオブジェクトは使用しない」と言い換えることができます。
そのため、この条件が成立している場合には Data クラスのオブジェクトとして正しく振る舞えなくてもよいのです。
逆に、それ以降もまだそのオブジェクトを使用したい場合はムーブすることはできないということです。

何か高度なメカニズムが用意されているのではないかと期待されていた方は拍子抜けされるかもしれませんが、ムーブの正体はこのようにシンプルなものです。

ムーブの構成要素

ここまでで、ムーブがどういったものかのイメージはお持ちいただけたと思います。
ここからは、ムーブを現実的に成立させるための要素をひとつずつ見ていきます。

一般化の必要性

ムーブの原理でご説明したような手法を独自に実装しているケースは以前からありました。
たとえば、 N1377 でも言及されているように標準ライブラリの std::list クラステンプレートの splice メンバ関数 [list.ops][8] がそれです。
splice は他の std::list オブジェクトから自身に要素を移動するものです。
複数オーバーロードされている中のいくつかは次のように規定されており、ムーブによる実装が想定されていることがうかがえます:

Complexity: Constant time.

しかし、ムーブするのに独自のメンバ関数を使うというこのようなスタイルはあまりよくありません。
たとえばクラス Hoge はメンバ関数 foo で, クラス Piyo はメンバ関数 bar で, それぞれムーブできるというような場合、両クラスの利用者はその都度リファレンスなどを参照してどのメンバ関数を呼べばよいのか確認する必要に迫られます。
何より、 C++ の強力な武器であるテンプレートが使えなくなってしまいます。

そのため、あらゆるクラスで同様の方法でムーブできることが望ましいです。
さらに、クラスだけではなく int のような組み込み型でも同じ方法が使えるようにしたいのは言うまでもないですね。

ポイントは次の 4 点くらいかなと思います。
これらが一般化・標準化されていれば、他人が書いたコード (他人から提供されるライブラリなど) と組み合わせてプログラムを作るような場合でも安心できますね。
未知のクラスであっても問題なくムーブできるテンプレートライブラリを書くこともできます。

  1. ムーブしてもかまわない、言い換えるとそれ以降は使用しないオブジェクトを識別する方法。
  2. そのようなオブジェクトを取り扱う方法。
  3. ムーブが発動するタイミングと条件。
  4. 実際にムーブする方法。

rvalue

まずは、ムーブしてもかまわない (それ以降は使用しない) オブジェクトを識別する方法です。
これまで極力控えてきましたが、そろそろ専門用語を登場させることをご容赦願います。

C++ には value category [basic.lval] という分類があります。
これは名前に反して値 (value) ではなく式 (expression) を分類したものですが、この中に lvalue と rvalue があります。

value category

実際の value category はもっと複雑に規定されています。
figure 6 は、 N4861 の 7.2.1 Value category の Figure 1 を転記したものです。
それぞれの分類の意味は本記事では触れません。

figure 6

怒られそうなくらい大雑把にご説明すると、名前があるオブジェクトは lvalue, 名前がないオブジェクトは rvalue です。
たとえば、 code 7 の main 関数内のオブジェクトを分類すると table 1 のようになります。

code 7
#include <iostream>

int main() {
  int a = 2;
  int b = 3;
  int c = a + b;
  std::cout << c << '\n';
}

table 1

lvalue rvalue
a, b, c, std::cout 2, 3, (a + b の計算結果), '\n'

2, 3, '\n' などのリテラルはいったん忘れて table 1 をご覧いただくと、変数 (これは名前があるオブジェクトですね) は lvalue で、 (a + b の計算結果) のように名前がないオブジェクトは rvalue であることが理解いただけると思います。

code 8 のようにちょっと書き換えた例でも、関数 sum が返す値は rvalue です。

code 8
#include <iostream>

int sum(int a, int b) {
  return a + b;
}

int main() {
  int a = 2;
  int b = 3;
  int c = sum(a, b);
  std::cout << c << '\n';
}

rvalue、つまり code 7 における (a + b の計算結果) や、 code 8 における (関数 sum が返す値) の役目はその場限りの一時的なもので、その後はもう使われないことに注目してください。

lvalue である変数 a, b, c は、スコープから抜けない限りあとから何度でも参照することができますが、 rvalue はそうではありませんね。
名前がないのであとからは使いようがないのです。

「どちらの rvalue も、変数 c に代入しているのであとから参照できるのでは?」とお思いかもしれませんが、変数 c と、それに代入された計算結果は別のものであることに注意してください。
計算結果そのものはあとからは使えません (参照できません) ね。

この性質を踏まえますと、 rvalue であればあとから使うことはできないのでムーブしてもかまわない、と考えることができます。
C++ では、 lvalue はムーブできない, rvalue はムーブできる, と解釈します。

くり返しとなりますが、計算や関数呼び出しの結果あるいはその過程で一時的に生成される名前がないオブジェクトを rvalue と分類し、 rvalue はあとから使うことができないのでムーブしても問題なく、 rvalue はムーブできるとみなします。

rvalue reference

次は、 rvalue に分類されるオブジェクトを取り扱う方法です。
ムーブするためには名前がない rvalue オブジェクトを操作できなくてはなりません。

そのために C++11 で導入されたのが rvalue reference [dcl.ref] です。
それまでにも存在していた reference は C++11 で lvalue reference と呼び方が変わり、 reference は lvalue reference と rvalue reference の総称となりました。

lvalue reference は型に続いて & を 1 個つけますよね。
rvalue reference は同様に && と 2 個つけます。
つまり、 int&int 型の lvalue オブジェクトを参照する lvalue reference で、 int&&int 型の rvalue オブジェクトを参照する rvalue reference です。

これで、名前がない rvalue オブジェクトを取り扱うことができるようになりました。
具体的なコードはこの次にご覧いただきます。

ムーブコンストラクタとムーブ代入演算子

続いて、ムーブが発動するタイミング・条件と、実際にムーブする方法です。

コピーの場合を考えてみますと、オブジェクトのコピーは初期化か代入のいずれかのタイミングで行われます。
クラスでそれらを実現するのがコピーコンストラクタ (copy constructor) とコピー代入演算子 (copy assignment operator) ですね。

ムーブも同様で、オブジェクトの初期化または代入のタイミングでムーブさせます。
そのために、やはり C++11 でムーブコンストラクタ (move constructor) [class.copy.ctor] とムーブ代入演算子 (move assignment operator) [class.copy.assign] が用意されました。

クラス C におけるこれらのコンストラクタと代入演算子のシグネチャは code 9 のとおりです。
コピーコンストラクタ, コピー代入演算子はコピー元 (引数 src) を const な lvalue reference で受け取ります。
一方、前述したようにムーブできるのは rvalue オブジェクトなので、ムーブコンストラクタ, ムーブ代入演算子はムーブ元 (引数 src) を rvalue reference で受け取ることに注目してください。
なお、これらムーブ操作ではムーブ元を変更し得るので普通は const はつけません。

code 9
class C {
 public:
  // copy constructor
  C(const C &src);
  // move constructor
  C(C &&src);

  // copy assignment operator
  C& operator=(const C &src);
  // move assignment operator
  C& operator=(C &&src);
};

コピーコンストラクタとムーブコンストラクタ, コピー代入演算子とムーブ代入演算子は、引数が lvalue reference か rvalue reference かだけの違いです。
初期化 C c = xxx; や代入 c = xxx; において、 xxx が lvalue であればコピーコンストラクタやコピー代入演算子が, xxx が rvalue であればムーブコンストラクタやムーブ代入演算子が, それぞれ呼び出されます (table 2)。

table 2

xxx は lvalue xxx は rvalue
初期化 (C c = xxx;) コピーコンストラクタ
(C::C(const C&))
ムーブコンストラクタ
(C::C(C&&))
代入 (c = xxx;) コピー代入演算子
(C::operator=(const C&))
ムーブ代入演算子
(C::operator=(C&&))

これは通常のオーバーロード解決と同様で、オーバーロードは型 (type) だけでなく value category によっても解決されます。

実際のムーブ操作はこれらムーブコンストラクタやムーブ代入演算子で行われます。
つまり、このクラスの提供者が実装します。
実装例を code 10 に示します。

code 10
class Data {
 public:
  // move constructor
  Data(Data &&src)
      : subData_(src.subData_) {
    src.subData_ = nullptr;
  }

  // move assignment operator
  Data& operator=(Data &&src) {
    if (&src == this) {
      return *this;
    }

    delete subData_;
    subData_ = src.subData_;
    src.subData_ = nullptr;
    return *this;
  }

 private:
  SubData *subData_;
};

ムーブコンストラクタでもムーブ代入演算子でも、それまでムーブ元 (引数 src) が持っていた subData_ を奪っています。
ムーブの原理でのご説明そのままです。

std::move

ここまでで述べてきたことをまとめます。

  • ムーブできるのは、名前がないためにあとから参照できない rvalue オブジェクトに限られる。
  • ムーブされるのはオブジェクトの初期化や代入のタイミングで、それぞれムーブコンストラクタやムーブ代入演算子が呼ばれる。
  • ムーブコンストラクタやムーブ代入演算子はムーブ元の rvalue オブジェクトを rvalue reference で受け取る。
  • ムーブコンストラクタやムーブ代入演算子で実際にムーブさせるのはクラスの提供者であるプログラマーの役割。

さて、ここでもうひとつご紹介したいものがあります。
ムーブできるのは rvalue オブジェクトに限られますが、実際には lvalue オブジェクトをムーブしたい場面は多いです。
変数も lvalue ですからごもっともですね。

そのような場合に使うのが std::move 関数テンプレート [forward][9] です。
<algorithm> ヘッダにも同名の関数テンプレートがありますが、ここでご説明するのは <utility> ヘッダの方です。

この std::move 関数テンプレートは code 11 のように規定されており、 static_cast<remove_reference_t<T>&&>(t) を返します。

code 11
template <class T> constexpr remove_reference_t<T>&& move(T&& t) noexcept;

T&& はご説明していませんが難しいことはなくて、 std::move は引数 t を単に rvalue reference にキャストするだけです。
キャストするだけなので、名前に反して std::move によってムーブされることはありません。
ただ、引数 t が lvalue reference であっても rvalue reference にキャストしてくれるので、 std::move を経由すればムーブコンストラクタやムーブ代入演算子が呼び出されて、結果的にムーブさせることができます。

やっていることはシンプルなので、自分で static_cast を書いても同じ効果が得られますが、コードを読んだ第三者が「ここで lvalue オブジェクトがムーブされているな」と気づくことができるように、素直に std::move を使うことをお勧めします。

ムーブの原理で登場した例で std::move を使ってみると code 12 のようになります。

code 12
#include <utility>
#include "data.hpp"

int main() {
  Data data1;
  Data data2 = std::move(data1);

  Data data3;
  Data data4;
  data4 = std::move(data3);
}

data1 オブジェクトは lvalue ですが、 std::move を経由させることで rvalue reference となり、 data2 オブジェクトの構築時にはムーブコンストラクタ (Data::Data(Data&&)) が呼ばれるようになります。
同様に、 data3 オブジェクトを std::move を経由させることで data4 オブジェクトへの代入時にムーブ代入演算子 (Data::operator=(Data&&)) が呼ばれるようになります。

この例でもし std::move を使わないと次のような動作となります。

  • data1 オブジェクトは lvalue であるため、 data2 オブジェクトの構築時にコピーコンストラクタ (Data::Data(const Data&)) が呼ばれ、 data1 オブジェクトは data2 オブジェクトにコピーされる。
  • data3 オブジェクトは lvalue であるため、 data4 オブジェクトへの代入時にコピー代入演算子 (Data::operator=(const Data&)) が呼ばれ、 data3 オブジェクトは data4 オブジェクトにコピーされる。

さて、 code 12 で注意すべきなのは、 std::move に渡したオブジェクト (ひとつめは data1, ふたつめは data3) は、それ以降は使ってはいけないということです。
どちらのオブジェクトも (もともと lvalue だったので) 名前があってあとから使えてしまうのですが、 std::move に渡したあとはムーブされているかもしれないため、決して使ってはいけません。
std::move はそれを表す目印でもあります。

ムーブの応用

ムーブの目的で述べたように、もともとムーブはコピーを効率化させる目的で考案され、標準化されました。
しかし、ムーブの性質が都合がよかったため、他の目的でも使われるようになりました。
その性質とは、ムーブとは何か?で PC のファイル操作のたとえでご説明した「ムーブしても総数は増えない」ことです。

それまでにも「コピーできない型」を作ることはできましたが、これは使い道がとても限定されてしまいます。 最初に構築した場所から動かせないわけですから。
ムーブの標準化によって「コピーはできないがムーブはできる型」を作れるようになったことで、応用範囲が広がったかたちです。

標準ライブラリが提供する「コピーはできないがムーブはできる型」の例[10]です:

  • std::basic_istream (ただし、ムーブコンストラクタは protected です)
  • std::basic_ostream (ただし、ムーブコンストラクタは protected です)
  • std::future
  • std::promise
  • std::thread
  • std::unique_lock
  • std::unique_ptr

これらの型に共通するのは、これらのオブジェクト自身あるいはこれらのオブジェクトが参照している別のオブジェクトが唯一であるということです。

たとえば、 std::thread オブジェクトはシステムのスレッドと 1 : 1 で対応します。
もし std::thread クラスがコピーできてしまうと、 1 本のスレッドに対応する std::thread オブジェクトが複数個存在してしまうことになり、 1 : 1 の対応関係が崩れてしまいます。
もちろん、新しい std::thread オブジェクトを新たに生成することはできますが、既存の std::thread オブジェクトを複製することができないということです。

コピーできないだけではなく、ムーブできることにもメリットがあり、たとえば std::thread オブジェクトを std::vector のようなコンテナに格納することができます。

別の例では、 std::unique_ptr クラステンプレートはいわゆるスマートポインタの一種で、単一所有権 (unique ownership) を実現するように設計されています。
対象のオブジェクトの所有者は常に唯一である (持ち主はひとりだけ) という意味です。
std::unique_ptr オブジェクトがコピーできてしまうと、対象のオブジェクトの所有者が複数になってしまい単一所有権が崩れてしまいます。

また、 std::unique_ptr オブジェクトをムーブすることで、オブジェクトとともに所有権も移動させることができます。
所有者は常にひとりだけですが、ムーブをうまく使うことで所有者を変更する (所有権を移転する) ことはできるようになっているわけです。

おわりに

本記事では C++ のムーブの概念をご説明しました。

後半は言語仕様や標準ライブラリをご説明するためにいくつかの専門用語を登場させましたが、前半だけでも「ムーブとはどのようなものか」をおおよそ理解いただけたのではないかと思います。

慣れるまではオブジェクトの関係を頭の中でイメージしてみると、ムーブすべきかどうか, そもそもムーブできるのかどうか, などが整理しやすくなると思います。


本記事は職場の後輩がくれた質問をきっかけに書きました。

参考文献

脚注
  1. Working Draft, Standard for Programming Language C++ [N4861]. http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/n4861.pdf. ↩︎

  2. X86アセンブラ/データ転送命令 - Wikibooks. https://ja.wikibooks.org/wiki/X86アセンブラ/データ転送命令. ↩︎

  3. Howard E. Hinnant, Peter Dimov, Dave Abrahams. A Proposal to Add Move Semantics Support to the C++ Language [N1377]. 2002. https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2002/n1377.htm. ↩︎

  4. More C++ Idioms/Copy-and-swap - Wikibooks, open books for an open world. https://en.wikibooks.org/wiki/More_C++_Idioms/Copy-and-swap. ↩︎

  5. Copy-and-swap - C++ Patterns. https://cpppatterns.com/patterns/copy-and-swap.html. ↩︎

  6. libstdc++ のソースを読めばすむのですが... ↩︎

  7. 24 Bytes の内訳は、 1. 要素を格納する領域の先頭アドレス (8 Bytes), 2. 要素を格納する領域の末尾の次のアドレス (8 Bytes), 3. 実際に格納されている要素数 (8 Bytes), だと考えられます。 ↩︎

  8. std::list<T,Allocator>::splice - cppreference.com. https://en.cppreference.com/w/cpp/container/list/splice. ↩︎

  9. std::move - cppreference.com. https://en.cppreference.com/w/cpp/utility/move. ↩︎

  10. 思いついたものだけです。 どこかに一覧はないでしょうか... ↩︎

Discussion