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

先頭へ戻る

この章の概要

この章の概要です。

move()

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

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

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

C++14

C++14 では以下のように、constexpr関数になりました。

namespace std {
    template <typename T>
    constexpr 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()

基本的にムーブ処理は例外(【言語解説】第40章)を送出しないように実装されるべきですが、例外を送出するムーブしか実装されていないことがあり得ます。そのような場合に 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つ目が条件の成否に応じて選択されます。

C++14

C++14 では以下のように、constexpr関数になりました。

namespace std {
    constexpr 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
}

例えば次のサンプルプログラムでは、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型ならどうでしょうか?


解答ページはこちら

参考リンク

更新履歴

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

'2017/8/26 新規作成。





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

次の章へ(第3章 unique_ptr)

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

Programming Place Plus のトップページへ


このエントリーをはてなブックマークに追加
rss1.0 取得ボタン RSS