オブジェクトのムーブ | Programming Place Plus 新C++編

トップページ新C++編

先頭へ戻る

このページの概要 🔗

このページでは、オブジェクトをムーブすることについて説明します。ムーブは、オブジェクトの値をコピーするのではなく移動させることで、実行効率を高めることを目的とした機能です。ムーブを実現するための方法として、ムーブコンストラクタとムーブ代入演算子、明示的なムーブ(std::move関数)を取り上げます。そのほか、値カテゴリや参照修飾子などを取り上げています。

このページの解説は C++14 をベースとしています

以下は目次です。要点だけをさっと確認したい方は、「まとめ」をご覧ください。



ムーブ 🔗

これまでのページでは、あるクラス型のオブジェクト ab があるとき、a = b のような代入はコピーを意味していました。つまり、代入を行ったあと、a の値は b と同じになり、b の値は変化しません。このような代入をコピー代入ともいい、「オブジェクトのコピー」のページの主題として説明しました。

しかし、a = b がコピーを意味しないケースがあります。それがこのページでの主題であるムーブ (move) と呼ばれる操作です。

a = b がムーブによる代入(ムーブ代入 (move assignment))である場合、代入を行ったあと、a の値は代入前の b と同じになり、b の方は値を失った状態になります。ムーブ、すなわち「移動」なので、値が b から a へと移動するイメージです。

実際のところ、本当の意味で値が(存在が)メモリから消え失せることはありませんが、データメンバが確保していたメモリ領域を手放すとか、データメンバがもはや無効なものであるといったふうに取り扱うといった意味でなら、あたかも値を失ったかのように振る舞わせることはできます。ムーブ代入の元になった側の値がまだそこに残っているかのように使うことはできませんが、オブジェクト自体はまだ残っています。そのため、最終的にはデストラクタが呼ばれることになります

ムーブ代入元になっていた変数に、ほかのオブジェクトを再代入することは、上書きしているだけなので問題ありません。


次のプログラムで、create_vector関数は、std::vector<int> 型の戻り値を返しており、コピーが発生するかのように思えます。

#include <iostream>
#include <vector>

std::vector<int> create_vector(std::size_t size, int value)
{
    std::vector<int> vec(size, value);
    // ...

    return vec;
}

int main()
{
    auto vec = create_vector(10, 7);  // 戻り値はコピーされている?

    for (int e : vec) {
        std::cout << e << "\n";
    }
}

実行結果:

7
7
7
7
7
7
7
7
7
7

コピーはオブジェクトのサイズによってはかなり時間が掛かるものになるため、コピーをできるだけ避けることが、プログラムの性能向上につながります。

実際には、create_vector関数が戻り値を返し、それを受け取り側が受け取る過程でコピーは発生していません。

まず、「オブジェクトのコピー」のページでも取り上げたように、コンパイラの最適化が働く可能性があります。その場合はコピーどころかムーブも発生しません。

そのような最適化が働かなかったとしても、C++11以降であれば、自動ストレージ期間を持つローカル変数を返すときにはムーブが優先され、それができなければコピーを選ぼうとします。std::vector はムーブに対応するように作られているので(これがどういうことは後述します)、コピーされる場面はなく、ムーブされることになります。

【上級】ムーブが優先されるのは、return文が返そうとしている自動ストレージ期間を持つローカル変数を rvalue として扱うためです[1]

【C++98/03 経験者】ムーブがなかった C++03以前では、コンパイラが最適化しなければ、当然コピーを行う動作になります。

std::vector のオブジェクトをコピーする場合、新たなメモリ領域を動的に割り当てたあと、すべての要素をそれぞれコピーすることになり、要素数によっては相当に大きな作業になります。一方、ムーブする場合は、ムーブ元のオブジェクトがすでに持っているメモリ領域と要素をそのまま引き継ぐだけで済みます。つまり、ポインタの指し示す先を切り替えるだけというレベルにまで作業量は激減します。その代わり、ムーブ元のオブジェクトの値が残っていることを期待して使うことはできなくなりますが、create_vector関数の例のように、ムーブ元にあったオブジェクトがすぐに消えてしまう場面ではまったく問題になりません。このようなプログラミング手法は、ムーブセマンティクス (move semantics) と呼ばれています。

重要なポイントの1つとして、ムーブがコピーに比べて効率的であるためには、ムーブ処理を効率的に記述できる必要があります。たとえば、int型のデータメンバを3個持つだけのクラスでは、コピーもムーブもまったく同じ処理を書くしかなく、処理効率が向上する余地はありません。std::vector はメモリを動的に割り当てる必要があり、ムーブでは再度の割り当てを避けられるので、処理効率の向上が期待できます。

また、さきほどのサンプルプログラムには、明示的にムーブさせることを命令しているコードはどこにもありません。つまり、ムーブに対応した型を、ムーブして構わないと思われる(ムーブ元のオブジェクトがもう使えない状態になってもいい)場面で使うときに、自動的にムーブが選択されているということです。自動的に効率の高い方法が使われているという意味では非常に便利ですが、何が起きているのか把握することが難しいともいえます。

ムーブコンストラクタ 🔗

さきほどの create_vector関数の例でムーブが使われるのは、std::vector がムーブに対応しているからと書きましたが、具体的にいえばムーブコンストラクタ (move constructor) が定義されているからです。

ムーブコンストラクタは、元になるオブジェクトをムーブしてきて、自身を初期化するコンストラクタです。以下のルールで宣言したコンストラクタは、ムーブコンストラクタであるとみなされます。[2]

rvalueリファレンス(右辺値参照) (rvalue reference) は && によって表される参照型の一種です。これまでのページで使ってきた & による参照型は区別のために、lvalueリファレンス(左辺値参照) (lvalue reference) と呼ばれることがあります。rvalueリファレンスの話はあとで再び取り上げることにして、まずはムーブコンストラクタの話を続けます。

vector(vector&& other);       // ムーブコンストラクタ
vector(const vector& other);  // コピーコンストラクタ

ただし、&& であっても、型推論を伴う場合には rvalueリファレンスとは異なるものとして解釈されます。たとえば、関数テンプレートの仮引数T に対する T&& や、型推論の auto に対する auto&& のような型が該当します。この話題は次のページで取り上げます。

このようにムーブコンストラクタとコピーコンストラクタの仮引数の違いがあるため、実引数がどちらに適合できるかによって両者の呼び分けがなされます。ムーブコンストラクタの方に適合できるのならムーブコンストラクタが呼ばれることになり、適合できなかったとしてもコピーコンストラクタが呼べるのならそちらが呼び出されることになります

create_vector関数の例に戻って考えると、戻り値として返されるものは一時オブジェクトです。あとで説明しますが、一時オブジェクトは rvalueリファレンスによって参照できるため、ムーブコンストラクタが選択されます。


ムーブコンストラクタは、以下の条件を満たす場合には、コンパイラが暗黙的に生成します[3]

つまり、ほかの特殊なメンバ関数の明示的な定義の有無によって影響されています。そのため、こうした特殊なメンバ関数を定義していないことで、ムーブコンストラクタが暗黙的に生成されたとしても、あとから特殊なメンバ関数の定義を書き足すことによって、暗黙的なムーブコンストラクタが失われる恐れがあります。前述したとおり、ムーブコンストラクタがなければコピーコンストラクタを呼び出そうとしますから、コンパイルエラーになるわけでもなく、非常に分かりづらい問題です。ムーブコンストラクタが呼び出されていたことによって受けていた高速化の恩恵がいつのまにか消えているかもしれません。そのため、ムーブコンストラクタが必要なのであれば、デフォルトの実装で構わないとしても、暗黙的な生成に任せず、=default を使って明示的に書くほうが確実といえます

なお、明示的にムーブコンストラクタを定義するのなら、あとで取り上げるムーブ代入演算子もあわせて定義することを検討するべきです[4]

ムーブコンストラクタが不要なら =delete で削除できます。

明示的にムーブコンストラクタ(やムーブ代入演算子)を定義すると、暗黙的なコピーコンストラクタやコピー代入演算子の生成は行われません(「オブジェクトのコピー」のページを参照)。

次のサンプルプログラムはムーブコンストラクタを明示的に定義しています(本来、ムーブ代入演算子も定義すべきですが、ここでは省略します)。

#include <iostream>

class MyValue {
public:
    explicit MyValue(int v);
    MyValue(const MyValue& other);  // コピーコンストラクタ
    MyValue(MyValue&& other);       // ムーブコンストラクタ
    ~MyValue();

    inline int get() const
    {
        return *m_pvalue;
    }

    inline bool has_value() const
    {
        return m_pvalue != nullptr;
    }

private:
    int*  m_pvalue;
};

MyValue::MyValue(int v) : m_pvalue {new int {v}}
{
}

MyValue::MyValue(const MyValue& other) : m_pvalue {new int {*other.m_pvalue}}
{
}

MyValue::MyValue(MyValue&& other) : m_pvalue {other.m_pvalue}
{
    other.m_pvalue = nullptr;  // ムーブ元のオブジェクトは値を失う
}

MyValue::~MyValue()
{
    delete m_pvalue;
}


MyValue create_myvalue(int v)
{
    return MyValue{v};
}

void print_myvalue(const MyValue& mv)
{
    if (mv.has_value()) {
        std::cout << mv.get() << "\n";
    } else {
        std::cout << "(no value)\n";
    }
}

int main()
{
    MyValue vp1 {create_myvalue(100)};
    MyValue vp2 {create_myvalue(200)};
    print_myvalue(vp1);
    print_myvalue(vp2);
}

実行結果:

100
200

コピーコンストラクタとムーブコンストラクタの実装の違いに注目してください。いずれもコンストラクタなので、新しく生成される側のデータメンバが正しく初期化されるように実装しなければならないのはもちろんのことですが、コピーコンストラクタではコピー元のオブジェクトが引き続き有効でなければならないのに対し、ムーブコンストラクタではムーブ元のオブジェクトは無効になるようにしなければなりません。

MyValueクラスのデータメンバは new によって動的に割り当てられるものです。ムーブでは、そのポインタを引き継ぐことで、再度の割り当てを避けることができます。そして、ムーブ元のポインタには nullptr を代入することにより無効化されたことを表現しています。nullptr で上書きしておくことには、デストラクタで delete が行われるときに何も起こらないようにする意味があり、重要です。

明示的なムーブ (std::move関数) 🔗

さきほどの MyValue のサンプルプログラムでは、ムーブコンストラクタが呼び出されるようにするため、create_myvalue関数を用意して、あえて一時オブジェクトを返すようにしていました(実際にはコンパイラの最適化によってコピーもムーブも消えてしまうかもしれません)。

一時オブジェクトを使って初期化するような場面では、自然とムーブコンストラクタを選択してもらえますが、そうでない場面でムーブコンストラクタを使ってほしいときには、明示的な記述が必要になります。たとえば、vp1 をムーブして vp2 を初期化したいと思って次のようにコードを書いても、これはコピーコンストラクタを呼び出してしまいます。

int main()
{
    MyValue vp1 {create_myvalue(100)};
    MyValue vp2 {vp1};  // コピー
    print_myvalue(vp1);
    print_myvalue(vp2);
}

実行結果:

100
100

vp1 は一時オブジェクトではなく、通常のオブジェクトです。このようなオブジェクトは rvalueリファレンスでは参照することができず、ムーブコンストラクタには適合しません。一方で vp1 を lvalueリファレンスでは参照できるので、コピーコンストラクタのほうが選択されます。結果として、コンパイルすることは可能ですが、これはコピーになります。

コピーコンストラクタとムーブコンストラクタの使い分けの理屈からいえば、このような場面でムーブコンストラクタを使わせるためにはキャストによって、強制的に rvalueリファレンスにすることが考えられます。以下の方法はうまくいきます。

int main()
{
    MyValue vp1 {create_myvalue(100)};
    MyValue vp2 {static_cast<MyValue&&>(vp1)};  // ムーブ
    print_myvalue(vp1);
    print_myvalue(vp2);
}

実行結果:

(no value)
100

とはいえキャストは見苦しいですし、意図も伝わりにくいといえます。そこで、std::move関数を使います。std::move関数は <utility> で宣言されている関数で、ムーブする意志があることを明示するために使用します。

int main()
{
    MyValue vp1 {create_myvalue(100)};
    MyValue vp2 {std::move(vp1)};
    print_myvalue(vp1);
    print_myvalue(vp2);
}

実行結果:

(no value)
100

std::move関数がしていることは、さきほどのコードと同じで、実引数を rvalueリファレンスにキャストして返すだけです。したがって、std::move関数自体がムーブを行っているわけではありません。

ムーブ代入 🔗

代入演算子の方も、コピーとムーブの使い分けがあります。

コピー代入の場合には、コピー代入演算子の演算子関数が呼び出されるのでした(「オブジェクトのコピー」のページを参照)が、ムーブ代入の場合には、ムーブ代入演算子 (move assignment operator) の演算子関数が呼び出されます。

ムーブ代入演算子は以下の条件を満たすとき、暗黙的に生成されます[5]

暗黙的に生成されるムーブ代入演算子は、次のように宣言されます[6]

public:
    inline C& operator=(C&&);

ムーブ代入演算子を明示的に定義する場合も、仮引数を rvalueリファレンスにします。戻り値は代入先のオブジェクトの参照(*this)なので lvalueリファレンスです。

ムーブ代入演算子が不要なら =delete で削除できるほか、=default で、暗黙的に生成されるものと同じ内容で明示的に宣言することもできます。

MyValueクラスにムーブ代入演算子を追加してみます。

#include <iostream>

class MyValue {
public:
    explicit MyValue(int v);
    MyValue(const MyValue& other);  // コピーコンストラクタ
    MyValue(MyValue&& other);       // ムーブコンストラクタ
    ~MyValue();

    MyValue& operator=(const MyValue& rhs);  // コピー代入演算子
    MyValue& operator=(MyValue&& rhs);       // ムーブ代入演算子

    inline int get() const
    {
        return *m_pvalue;
    }

    inline bool has_value() const
    {
        return m_pvalue != nullptr;
    }

private:
    int*  m_pvalue;
};

MyValue::MyValue(int v) : m_pvalue {new int {v}}
{
}

MyValue::MyValue(const MyValue& other) : m_pvalue {new int {*other.m_pvalue}}
{
}

MyValue::MyValue(MyValue&& other) : m_pvalue {other.m_pvalue}
{
    other.m_pvalue = nullptr;  // ムーブ元のオブジェクトは値を失う
}

MyValue::~MyValue()
{
    delete m_pvalue;
}

MyValue& MyValue::operator=(const MyValue& rhs)
{
    if (this != &rhs) {
        MyValue temp {rhs};
        std::swap(m_pvalue, temp.m_pvalue);
    }
    return *this;
}

MyValue& MyValue::operator=(MyValue&& rhs)
{
    if (this != &rhs)
    {
        // 以前のオブジェクトを解放して、ムーブ元の内容を引き継ぐ
        delete m_pvalue;
        m_pvalue = rhs.m_pvalue;
        rhs.m_pvalue = nullptr;  // ムーブ元のオブジェクトは値を失う
    }
    return *this;
}

void print_myvalue(const MyValue& mv)
{
    if (mv.has_value()) {
        std::cout << mv.get() << "\n";
    } else {
        std::cout << "(no value)\n";
    }
}

int main()
{
    MyValue vp1(100);
    MyValue vp2(0);

    vp2 = vp1;             // コピー代入
    print_myvalue(vp1);
    print_myvalue(vp2);

    vp2 = std::move(vp1);  // ムーブ代入
    print_myvalue(vp1);
    print_myvalue(vp2);
}

実行結果:

100
100
(no value)
100

ムーブコンストラクタの実装と考え方は同じで、ムーブ元オブジェクトの内容を効率よく引継ぎ、ムーブ後にはムーブ元オブジェクトが無効な状態になるように実装します。データメンバの m_pvalue には、最終的にデストラクタで delete されることを見越して、nullptr で上書きしておくことが必要です。

ムーブと例外安全 🔗

ムーブは通常、例外を送出しないように実装します。ムーブでは、新たなメモリを確保したり、複雑な処理を行ったりする必要性がないはずなので、例外を送出しないように実装することは難しくありません。明示的に noexcept を使って、例外を送出しない意思を示しておきましょう。

MyValue::MyValue(MyValue&& other) noexcept : m_pvalue {other.m_pvalue}
{
    other.m_pvalue = nullptr;  // ムーブ元のオブジェクトは値を失う
}

MyValue& MyValue::operator=(MyValue&& rhs) noexcept
{
    if (this != &rhs)
    {
        // 以前のオブジェクトを解放して、ムーブ元の内容を引き継ぐ
        delete m_pvalue;
        m_pvalue = rhs.m_pvalue;
        rhs.m_pvalue = nullptr;  // ムーブ元のオブジェクトは値を失う
    }
    return *this;
}

デストラクタも暗黙的に noexcept ですから(「例外」のページを参照)、delete のところで例外が送出されることもないはずです。

std::move関数も noexcept が指定されています。

ムーブの途中で例外を送出する可能性があると、例外安全にムーブできません(「例外」のページを参照)。たとえば、そのオブジェクトが複数のデータメンバを持っていて、それぞれをムーブしなければならないとしたら、作業の途中で例外が発生してしまうと、それまでにムーブしてしまったデータメンバは失われ、取り戻せないからです。これがコピーであったなら、コピー元が残っているため例外安全(強い保証)を維持できます。

そこで、noexcept なムーブが可能ならムーブを、それができなければコピーを選ぶような方法があれば便利です。これを実現する std::move_if_noexcept関数が <utility> で提供されています。

#include <iostream>
#include <utility>

class A {
public:
    A() {}
    A(const A&)  { std::cout << "copy\n"; }    // コピー
    A(A&&) noexcept { std::cout << "move\n"; } // noexcept なムーブ
};

class B {
public:
    B() {}
    B(const B&)  { std::cout << "copy\n"; }       // コピー
    B(B&&) { std::cout << "move (may throw)\n"; } // noexcept のないムーブ
};

int main()
{
    A a1 {};
    B b1 {};

    A a2 {std::move_if_noexcept(a1)}; // A&& で返されるため、ムーブコンストラクタが呼ばれる
    B b2 {std::move_if_noexcept(b1)}; // const B& で返されるため、コピーコンストラクタが呼ばれる
}

実行結果:

move
copy

std::move_if_noexcept関数は std::move関数と同じように、ムーブしたいときに呼び出せばいいです。std::move関数と同様、この関数自体がムーブやコピーをするのではなく、それらの操作へとつなげられるように適切な型に直して返すことが役割です。

noexcept なムーブができる場合は std::move関数と同じで、実引数を rvalueリファレンスにキャストした結果を返します。noexcept なムーブはできないが、コピーはできる場合には、コピー操作になるように、実引数を const lvalueリファレンスとして返します。

したがって、std::move_if_noexcept関数の戻り値の型は T&& あるいは const T& のいずれかになります。もちろん、C++ の関数の戻り値は1つだけなので、条件次第で動的に切り替わるということではなく、コンパイルの時点で1つに決定されています。

ムーブすることが選ばれたなら T&&型で返されることにより、ムーブコンストラクタやムーブ代入演算子の呼び出しへつなげられます。コピーが選ばれたなら const T&型で返されることにより、コピーコンストラクタやコピー代入演算子の呼び出しへつなげられます。


また、例外を送出しない交換の処理が定義できるのならば、例外を送出しないムーブ代入演算子を交換の手法で実装することができます。

MyValue& MyValue::operator=(MyValue&& rhs) noexcept
{
    std::swap(m_pvalue, rhs.m_pvalue);
    return *this;
}

この方法の場合、ムーブ元は、ムーブ先オブジェクトが以前に持っていたデータメンバを受け取ることになります。オブジェクトが無効になったことは伝わりづらくなりますが、ムーブ代入演算子の処理の中で delete を行う必要はなくなり、以前の値はムーブ元オブジェクト側のデストラクタによって適切に解放されます。自分自身とムーブによる交換を行うことは、値が順次移動していくだけであって実質何も起きていないので、this != &rhs のような自己代入のチェックも不要であり、前にあげたムーブ代入演算子の実装よりも簡潔かつ効率的です。

値カテゴリ 🔗

rvalueリファレンスと lvalueリファレンスの違いは、参照できる相手の違いです。rvalueリファレンスは rvalue と呼ばれる値を参照でき、lvalueリファレンスは lvalue と呼ばれる値を参照できます。ただし、const lvalueリファレンスは例外的に rvalue を参照することもできます。

では、rvalue と lvalue はどう違うのでしょう。C++ には値カテゴリ (value category) と呼ばれる、式を分類するルールがあり、以下のように分類されています[7]。xvalue は2回登場しています。

すべての式は lvalue、xvalue、prvalue のいずれかに属します[8]。glvalue、rvalue はそれぞれをまとめた上位カテゴリです。

glvalue は、一般的な lvalue の意味で「generalized lvalue」を略した名称です。lvalue と xvalue をまとめた呼び名です。

lvalue は、代入式の左辺側に置ける値を意味する左辺値 (left value) から来ている名前ですが、この考え方では正しく分類できないため、lvalue と呼ぶことを勧めます。オブジェクトや、ポインタや参照が指す先にあるオブジェクト、関数が該当します。

xvalue は、消失値などと訳される「expiring value」から来ている名称で、直後に寿命が尽きるようなオブジェクトを表現します。寿命が尽きるということは、もう不要であり、無効になって構わないということなので、xvalue はムーブできます

rvalue は、代入式の右辺側に置ける値を意味する右辺値 (right value) から来ている名前ですが、やはりこの考え方では正しく分類できないため、rvalue と呼ぶことを勧めます。一時オブジェクトはここに該当し、ムーブできるかどうかによって、prvalue であったり xvalue であったりします。

prvalue は、純粋な rvalue の意味で「pure rvalue」を略した名称です。rvalue のうち、xvalue ではないことを意味するもので、リテラルや、参照型でない戻り値を持つ関数の呼び出し結果が該当します。

【C言語プログラマー】C言語でも左辺値や右辺値という用語を使うときがありますが、C++ 値カテゴリのような分類はないので、別物と捉えたほうがいいでしょう[9]


次のようなコードはコンパイルできません。

int main()
{
    int& r {100};  // エラー
}

100 というリテラルを lvalueリファレンスで参照しようとしていますが、リテラルは prvalue であって lvalue ではないため、lvalueリファレンスでは参照できません。

prvalue を参照するために代わりとなる方法が2つあります。1つは const な lvalueリファレンスを使うことです。これは rvalueリファレンスが存在しなかった古い C++ で使われていた方法で、いわば例外的ルールのようなものです。

int main()
{
    const int& r {100};  // OK
}

もう1つの方法は、rvalue を参照することに特化した rvalueリファレンスを用いることです。

int main()
{
    int&& r {100};  // OK
}

100 はリテラルであって、名前によって参照できるオブジェクトではないため、このままではリファレンスと結び付ける記述はできません。そのため、一時オブジェクトが生成され、その一時オブジェクトを rvalueリファレンスが参照することになります。

一時オブジェクトはそのまま消えてしまうものなので、int&& r {100}; として r を初期化できるにしても、その後は消えてしまった一時オブジェクトを参照してしまうように思えます。

実際には、(以下の上級コラムにあげるような例外的な場面もわずかにありますが)リファレンスによって参照された一時オブジェクトの寿命は、そのリファレンスの寿命まで延長されるというルールがあるため[10]、リファレンス r が寿命を終えるまで一時オブジェクトは生存できます。

この文脈ではよく、「リファレンスによって束縛 (bind) する」という表現をします。

【上級】例外的な場面の例としては、リファレンス型のデータメンバによって一時オブジェクトを束縛したときがあります[10]。こうした例外的な場面があるので、必ず寿命が延長されるわけではないですが、典型的な場面では安全です。

#include <iostream>

int main()
{
    int&& r {100};
    std::cout << r << "\n";  // OK。r が参照する 100 を使える
}

実行結果:

100

参照修飾子 🔗

非静的なメンバ関数には、参照修飾子 (reference qualifier) を付けることができます。参照修飾子は、メンバ関数を呼び出すオブジェクトの値カテゴリを制限するために使用します。

参照修飾子には以下の2種類があります。

参照修飾子はメンバ関数の宣言および定義の末尾に付けます。

class C {
public:
    void func() &;
    void func() &&;
};

このコード例のように、種類が異なる参照修飾子を持ったメンバ関数はオーバーロードできます。

#include <iostream>

class C {
public:
    void func() &
    {
        std::cout << "lvalue\n";
    }

    void func() &&
    {
        std::cout << "rvalue\n";
    }
};

int main()
{
    C c{};
    c.func();    // c は lvalue

    C{}.func();  // C{} は一時オブジェクトなので rvalue
}

実行結果:

lvalue
rvalue

この例の場合、2つのメンバ関数は constメンバ関数ではないので、const なオブジェクトからでは呼び出せませんが、参照修飾子と const を併用することができるので、const版とそうでないものをそれぞれ定義することで使い分けが可能です。

#include <iostream>

class C {
public:
    void func() &
    {
        std::cout << "lvalue\n";
    }

    void func() const &
    {
        std::cout << "const lvalue\n";
    }

    void func() &&
    {
        std::cout << "rvalue\n";
    }
};

int main()
{
    C c{};
    c.func();    // c は lvalue

    C{}.func();  // C{} は一時オブジェクトなので rvalue

    const C cc{};
    cc.func();   // cc は const lvalue
}

実行結果:

lvalue
rvalue
const lvalue

volatile、noexcept などと併用することもできます。記述する順番は自由です。

rvalue を const lvalueリファレンスで参照できることと同じ理由で、& の参照修飾子を付加された constメンバ関数があると、rvalue オブジェクトからでも呼び出せることに注意してください。

#include <iostream>

class C {
public:
    void func() &
    {
        std::cout << "lvalue\n";
    }

    void func() const &
    {
        std::cout << "const lvalue\n";
    }
};

int main()
{
    C c{};
    c.func();    // c は lvalue

    C{}.func();  // C{} は一時オブジェクトなので rvalue

    const C cc{};
    cc.func();   // cc は const lvalue
}

実行結果:

lvalue
const lvalue
const lvalue

まとめ 🔗


新C++編の【本編】の各ページには、末尾に練習問題があります。ページ内で学んだ知識を確認する簡単な問題から、これまでに学んだ知識を組み合わせなければならない問題、あるいは更なる自力での調査や模索が必要になるような高難易度な問題をいくつか掲載しています。


参考リンク 🔗


練習問題 🔗

問題の難易度について。

★は、すべての方が取り組める入門レベルの問題です。
★★は、自力でプログラミングができるようなるために、入門者の方であっても取り組んでほしい問題です。
★★★は、本格的にプログラマーを目指す人のための問題です。

問題1 (確認★)

オブジェクトのムーブを禁止したクラスはどのように作成しますか?

解答・解説

問題2 (確認★)

次のプログラムで、c1~c4 の初期化に使われるコンストラクタはそれぞれどれであるか答えてください。

#include <iostream>
#include <utility>

class C {
public:
    C()
    {
        std::cout << "default constructor\n";
    }

    C(const C&)
    {
        std::cout << "copy constructor\n";
    }

    C(C&&)
    {
        std::cout << "move constructor\n";
    }
};

C f()
{
    C c {};
    return c;
}

int main()
{
    C c1 {};
    C c2 {c1};
    C c3 {f()};
    C c4 {std::move(c1)};
}

解答・解説

問題3 (基本★★)

データメンバが大きくなることが予想される BigDataクラスを次のように定義しました。

class BigData {
public:
    BigData();
    explicit BigData(std::size_t n, int value);  // 値が value の n個の要素を持つように初期化
    BigData(const BigData&);
    BigData(BigData&&) noexcept;
    BigData& operator=(const BigData&); 
    BigData& operator=(BigData&&) noexcept;
    ~BigData();

    std::vector<int> process() const;  // 重い処理を行い、結果を返す
    std::size_t size() const;

private:
    std::vector<int> m_data;
};

それぞれのメンバ関数を実装してください。processメンバ関数の内容は適当で構いません。

解答・解説

問題4 (基本★★)

問題3の BigDataクラスについて、processメンバ関数の具体的な処理内容には触れませんが、この関数がデータメンバ m_data を使った何かしらかの処理のあと、その結果となる std::vector<int> を返すとすれば、戻り値についてのコピーやムーブが行われるかどうかを説明してください。


解答・解説ページの先頭



更新履歴 🔗




はてなブックマーク に保存 Facebook でシェア X で ポストフォロー
LINE で送る noteで書く rss1.0 取得ボタン RSS
管理者情報 プライバシーポリシー
先頭へ戻る