ムーブに関するユーティリティ | Programming Place Plus Modern C++編【標準ライブラリ】 第2章

トップページModern C++編

Modern C++編は作りかけで、更新が停止しています。代わりに、C++14 をベースにして、その他の方針についても見直しを行った、新C++編を作成しています。
Modern C++編は削除される予定です。

この章の概要 🔗

この章の概要です。


move関数 🔗

std::move関数 は、ムーブを実現するために、実引数を右辺値に変換して返す関数です。ムーブという言語機能に関しては、【言語解説】第14章を参照してください。

std::move関数は、<utility> という標準ヘッダで宣言されています。これまでに解説されていない機能が使われていますが、宣言は以下のようになっています。

namespace std {
    template <typename T>
    typename remove_reference<T>::type&& move(T&& t) noexcept;
}

std::move関数は、実引数の値を static_cast を使って右辺値参照に変換して返しているだけです。返される右辺値参照を、右辺値参照を受け取ることができるムーブコンストラクタやムーブ代入演算子へ渡せばムーブが実現されます。

#include <iostream>
#include <utility>

class MyClass {
public:
    MyClass() = default;
    MyClass(const MyClass& rhs)
    {
        std::cout << "copy construtor" << std::endl;
    }
    MyClass(MyClass&& rhs)
    {
        std::cout << "move construtor" << std::endl;
    }
    
    ~MyClass() = default;

    MyClass& operator=(const MyClass& rhs)
    {
        std::cout << "copy" << std::endl;
        return *this;
    }
    MyClass& operator=(MyClass&& rhs)
    {
        std::cout << "move" << std::endl;
        return *this;
    }
};

int main()
{
    MyClass a;
    MyClass b(a);
    MyClass c(std::move(a));

    a = b;
    a = std::move(b);
}

実行結果:

copy construtor
move construtor
copy
move

move_if_noexcept関数 🔗

基本的にムーブ処理は例外を送出しないように実装されるべきですが、例外を送出するムーブしか実装されていないことがあり得ます。そのような場合に std::move関数を使うと、例外安全の面で保証が持てなくなってしまうので、その対策のためにあるのが std::move_if_noexcept関数 です。

std::move_if_noexcept関数は、実引数の型が例外を送出しないムーブが可能な場合にだけ右辺値参照を返し、そうでない場合には const左辺値参照を返すように実装されています。つまり場合によって、戻り値の型が異なるということです。詳しく理解する必要はありませんが、これは以下のような宣言によって実現されています。

namespace std {
    typename std::conditional<  
        !std::is_nothrow_move_constructible<T>::value && std::is_copy_constructible<T>::value,
        const T&,
        T&&
    >::type move_if_noexcept(T& x) noexcept
}

std::conditional を使うことで、コンパイル時に判断できる条件によって、2通りの型を導き出しています。std::conditional のテンプレート実引数が3つあり、1つ目が条件で、2つ目と3つ目が条件の成否に応じて選択されます。

たとえば次のサンプルプログラムでは、MyClass にはムーブコンストラクタとムーブ代入演算子が明示的に実装されていますが、いずれも noexcept ではないため、ムーブではなくコピーが選択されます。

#include <iostream>
#include <utility>

class MyClass {
public:
    MyClass() = default;
    MyClass(const MyClass& rhs)
    {
        std::cout << "copy construtor" << std::endl;
    }
    MyClass(MyClass&& rhs)
    {
        std::cout << "move construtor" << std::endl;
    }
    
    ~MyClass() = default;

    MyClass& operator=(const MyClass& rhs)
    {
        std::cout << "copy" << std::endl;
        return *this;
    }
    MyClass& operator=(MyClass&& rhs)
    {
        std::cout << "move" << std::endl;
        return *this;
    }
};

int main()
{
    MyClass a;
    MyClass b(a);
    MyClass c(std::move_if_noexcept(a));

    a = b;
    a = std::move_if_noexcept(b);
}

実行結果:

copy construtor
copy construtor
copy
copy

ムーブコンストラクタとムーブ代入演算子が noexcept ならば、それらを選択します。

#include <iostream>
#include <utility>

class MyClass {
public:
    MyClass() = default;
    MyClass(const MyClass& rhs)
    {
        std::cout << "copy construtor" << std::endl;
    }
    MyClass(MyClass&& rhs) noexcept
    {
        std::cout << "move construtor" << std::endl;
    }
    
    ~MyClass() = default;

    MyClass& operator=(const MyClass& rhs)
    {
        std::cout << "copy" << std::endl;
        return *this;
    }
    MyClass& operator=(MyClass&& rhs) noexcept
    {
        std::cout << "move" << std::endl;
        return *this;
    }
};

int main()
{
    MyClass a;
    MyClass b(a);
    MyClass c(std::move_if_noexcept(a));

    a = b;
    a = std::move_if_noexcept(b);
}

実行結果:

copy construtor
move construtor
copy
move

ムーブ可否の判定 🔗

ある型で、ムーブコンストラクタやムーブ代入演算子が使用できるかどうかを調べる手段があります。これは先ほどの std::move_if_noexcept関数 でも利用されている方法です。

std::is_move_constructible はムーブコンストラクタが使用できるかどうかを、std::is_move_assignable はムーブ代入演算子が使用できるかどうかを判定します。また、例外が送出されないかどうかを含めた判定が行える std::is_nothrow_move_constructiblestd::is_nothrow_move_assignable もあります。

これらは関数ではなく、構造体テンプレートです。なおいずれも、<type_traits> という標準ヘッダにあります。

namespace std {
    template <typename T>
    struct is_move_constructible;
    
    template <typename T>
    struct is_move_assignable;

    template <typename T>
    struct is_nothrow_move_constructible;
    
    template <typename T>
    struct is_nothrow_move_assignable;
}

ここでは詳しい仕組みについては触れませんが、テンプレート仮引数 T に、判定対象の型を当てはめるように使います。すると、要件を満たしていれば value というメンバが true になり、満たしていなければ false になります。

std::is_move_constructible や std::is_nothrow_move_constructible は T&& を使って T を生成できるかどうか、std::is_move_assignable や std::is_nothrow_move_assignable は T&& を T型へ代入できるかどうかを判定しています。

また、std::move_if_noexcept関数が戻り値の型を決定するために使っていることから分かるように、これらの判定機能はコンパイル時に処理を終えられます。

#include <iostream>
#include <type_traits>

class MyClass {
public:
    MyClass() = default;
    MyClass(const MyClass&) = default;
    MyClass(MyClass&&) = default;
    ~MyClass() = default;

    MyClass& operator=(const MyClass&) = default;
    MyClass& operator=(MyClass&&) = default;
};

int main()
{
    std::cout << std::is_move_constructible<MyClass>::value << "\n"
              << std::is_move_assignable<MyClass>::value << "\n"
              << std::is_nothrow_move_constructible<MyClass>::value << "\n"
              << std::is_nothrow_move_assignable<MyClass>::value << std::endl;
}

実行結果:

1
1
1
1

ムーブコンストラクタとムーブ代入演算子をデフォルトのものにしていると、使用可能かつ例外送出もしないため、すべての判定が true になりました。先ほど書いたとおり、メンバの value が true になるか false になるかは、コンパイルの時点で確定しています。つまり、main関数を次のように書いていたのと同じであり、実行時に行う処理はありません。

int main()
{
    std::cout << true << "\n"
              << true << "\n"
              << true << "\n"
              << true << std::endl;
}

ムーブコンストラクタとムーブ代入演算子をを「=delete」で削除してやると、すべての判定が false になります。

#include <iostream>
#include <type_traits>

class MyClass {
public:
    MyClass() = default;
    MyClass(const MyClass&) = default;
    MyClass(MyClass&&) = delete;  // 削除
    ~MyClass() = default;

    MyClass& operator=(const MyClass&) = default;
    MyClass& operator=(MyClass&&) = delete;  // 削除
};

int main()
{
    std::cout << std::is_move_constructible<MyClass>::value << "\n"
              << std::is_move_assignable<MyClass>::value << "\n"
              << std::is_nothrow_move_constructible<MyClass>::value << "\n"
              << std::is_nothrow_move_assignable<MyClass>::value << std::endl;
}

実行結果:

0
0
0
0

また、例外を投げ得る形(noexcept を付加しない)で明示的に実装すると、std::is_nothrow_move_constructible と std::is_nothrow_move_assignable による判定結果だけが false になることが分かります。

#include <iostream>
#include <type_traits>

class MyClass {
public:
    MyClass() = default;
    MyClass(const MyClass&) = default;
    MyClass(MyClass&&) {}
    ~MyClass() = default;

    MyClass& operator=(const MyClass&) = default;
    MyClass& operator=(MyClass&&) { return *this; }
};

int main()
{
    std::cout << std::is_move_constructible<MyClass>::value << "\n"
              << std::is_move_assignable<MyClass>::value << "\n"
              << std::is_nothrow_move_constructible<MyClass>::value << "\n"
              << std::is_nothrow_move_assignable<MyClass>::value << std::endl;
}

実行結果:

1
1
0
0


swap関数 🔗

std::swap関数 は、2つの値を交換する関数です。<utility> という標準ヘッダで、以下のように宣言されています。

namespace std {
    template <typename T>
    void swap(T& a, T& b) noexcept(
        std::is_nothrow_move_constructible<T>::value &&
        std::is_nothrow_move_assignable<T>::value);

    template <typename T, size_t N>
    void swap(T (&a)[N], T (&b)[N]) noexcept(noexcept(swap(*a, *b)));
}

非常に複雑なようですが、noexcept のところを無視すれば、上の関数は T型同士、下の関数は T型で要素数 N の配列同士で交換ができるということです。

【上級】noexcept のところですが、上の関数は、T型に、例外を送出しないムーブコンストラクタとムーブ代入演算子があれば、swap関数も例外を送出しないことを宣言しています。下の関数も意味は同じで、配列の要素の型(結局は T型のこと)が、例外を送出しないムーブコンストラクタとムーブ代入演算子があれば、swap関数も例外を送出しないことを宣言しています。

#include <iostream>
#include <utility>

class MyClass {
public:
    MyClass(int v) : mValue(v) {}

    inline int GetValue() const
    {
        return mValue;
    }

private:
    int mValue;
};

int main()
{
    MyClass a(10);
    MyClass b(20);
    std::swap(a, b);
    std::cout << a.GetValue() << ", " << b.GetValue() << std::endl;

    int c[] = {0, 1, 2, 3, 4};
    int d[] = {5, 5, 5, 5, 5};
    std::swap(c, d);
    for (int i = 0; i < 5; ++i) {
        std::cout << c[i] << ", " << d[i] << std::endl;
    }
}

実行結果:

20, 10
5, 0
5, 1
5, 2
5, 3
5, 4

swap関数の実装は、作業用変数を用いた典型的な交換のアルゴリズムですが、std::move関数を使って入れ替えを行うようになっています。

template <typename T>
void swap(T& a, T& b) noexcept(
    std::is_nothrow_move_constructible<T>::value &&
    std::is_nothrow_move_assignable<T>::value)
{
    T tmp = std::move(a);
    a = std::move(b);
    b = std::move(tmp);
}

このような実装であるため、ムーブが使えるのならばムーブで交換が行われ、使えない場合はコピーで交換されます。利便性を損なうことなく、最高の性能を発揮できるようになっています。


練習問題 🔗

問題① コピーやムーブに関するメンバ関数を定義したりしなかったりすることで、std::swap関数の挙動がどう変わるか確認してください。

問題② int型はムーブによる生成や、ムーブ代入ができますか? また、const int型ならどうでしょうか?


解答ページはこちら

参考リンク 🔗


更新履歴 🔗

 コンパイラの対応状況について、対応している場合は明記しない方針にした。

 新規作成。



前の章へ (第1章 C標準ライブラリ)

次の章へ (第3章 unique_ptr)

Modern C++編のトップページへ

Programming Place Plus のトップページへ



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