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

トップページ新C++編オブジェクトのムーブ

このページの概要 🔗

このページは、練習問題の解答例や解説のページです。



解答・解説 🔗

問題1 (確認★) 🔗

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


オブジェクトをムーブする方法として、ムーブコンストラクタ(本編解説)とムーブ代入演算子(本編解説)があります。そのためこの2つが使えないようにすれば、オブジェクトのムーブを禁止できます。

ムーブコンストラクタもムーブ代入演算子も、コンパイラが暗黙的に生成することがある特殊なメンバ関数の一種なので、=delete を使って削除することができます(「コンストラクタ」「静的メンバ」のページを参照)。

#include <iostream>
#include <utility>

class DataStore {
public:
    explicit DataStore(int v) : m_value {v}
    {}
    
    // ムーブコンストラクタを削除
    DataStore(DataStore&& other) = delete;

    // ムーブ代入演算子を削除
    DataStore& operator=(DataStore&& rhs) = delete;

private:
    int    m_value;
};

int main()
{
    DataStore ds1 {100};
    DataStore ds2 {std::move(ds1)};  // エラー。ムーブコンストラクタは削除されている
    ds2 = std::move(ds1);            // エラー。ムーブ代入演算子は削除されている
}

問題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)};
}


c1 はもちろんデフォルトコンストラクタ(C())が使われます。

c2 は、c1 が lvalue であるため(本編解説)、仮引数が const C& であるコピーコンストラクタに適合します。& によって表現される参照型は lvalueリファレンスであり、lvalue を参照できるためです。

c3 は、f関数の戻り値を使っています。これは一時オブジェクトであり、rvalue に該当します(本編解説)。この場合、仮引数が rvalueリファレンス(C&&)であるムーブコンストラクタと、const lvalueリファレンス(const C&)であるコピーコンストラクタの両方に適合しますが、rvalueリファレンスの方がより適合度が高いため、ムーブコンストラクタが選ばれます。なお、コンパイラの最適化が働く場合は、コピーもムーブも発生せず、デフォルトコンストラクタだけが呼び出されることがあります(本編解説)。

c4 は、lvalue である c1 が元になっていますが、std::move関数を通すことで、rvalueリファレンスにキャストされます(本編解説)。そのため、ムーブコンストラクタが選ばれます。


実行結果は次のようになります。

default constructor
copy constructor
default constructor  <-- f関数内の c の生成
move constructor
move constructor

問題3 (基本★★) 🔗

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

class BigData {
public:
    BigData();
    BigData(const BigData&);
    BigData(BigData&&) noexcept;
    BigData& operator=(const BigData&); 
    BigData& operator=(BigData&&) noexcept;
    ~BigData();

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

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

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


たとえば次のように実装できます。

#include <iostream>
#include <utility>
#include <vector>

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

    std::vector<int> process() const;
    
    inline std::size_t size() const
    {
        return m_data.size();
    }

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

BigData::BigData(std::size_t n, int value) : m_data(n, value)
{
}

BigData::BigData(BigData&& other) noexcept
    : m_data {std::move(other.m_data)}
{
}

BigData& BigData::operator=(BigData&& other) noexcept
{
    std::swap(m_data, other.m_data);
    return *this;
}

std::vector<int> BigData::process() const
{
    std::vector<int> result {};
    result.reserve(m_data.size());

    // ここでは例として、全要素を2倍にして返す
    for (int x : m_data) {
        result.push_back(x * 2);
    }

    return result;
}


int main()
{
    BigData big_data(1000000, 3);
    auto result = big_data.process();

    BigData big_data2 {std::move(big_data)};

    std::cout << big_data.size() << " " << big_data2.size() << "\n";
}

実行結果:

0 1000000

std::vector<int> はムーブコンストラクタとムーブ代入演算子を持っているため、BigDataクラスのムーブコンストラクタとムーブ代入演算子では、データメンバ m_data を std::move を使ってムーブするだけです。

問題4 (基本★★) 🔗

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


processメンバ関数を、問題3の解答例のように実装したとします。

std::vector<int> BigData::process() const
{
    std::vector<int> result {};
    result.reserve(m_data.size());

    // ここでは例として、全要素を2倍にして返す
    for (int x : m_data) {
        result.push_back(x * 2);
    }

    return result;
}

// 呼び出し例
auto result = big_data.process();

C++14 においては保証はありませんが、コンパイラは戻り値の最適化(RVO)を行うことが多く(「オブジェクトのコピー」のページを参照)、その場合、戻り値を返す際にコピーもムーブも発生しません(本編解説)。

【C++17】C++17以降では、ローカル変数をそのまま返すかたちであれば、この最適化が保証されます。

もし最適化が行われなかった場合、std::vector<int> にはムーブコンストラクタがあり、ローカル変数result をムーブすることが可能であるため、ここではムーブが行われることになります。

まだムーブが存在していなかった C++03以前であれば、コピーされることになります。

これらの動作は、BigDataクラス自体のムーブコンストラクタやムーブ代入演算子の実装とは関係がありません。


参考リンク 🔗



更新履歴 🔗




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