SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

Modern C++入門

メモリ利用を効率化! Modern C++のキモ「ムーブセマンティクス」

第4回 コピーと移動は明確に使い分けよう

  • X ポスト
  • このエントリーをはてなブックマークに追加

ムーブで何ができる?

 コピーについて紹介してきたところで、ここでいよいよムーブです。上のshallow_copy_null.cppの例で見てきたように、コピー後にオリジナルを残す必要がない場合は、ポインタの受け渡しで済ませた方が、実行効率が上がります。そこで、オリジナルを残すコピーに対して、実行効率の向上を目的に移動で済ませるムーブという考え方がC++ 11で生まれました。ムーブを利用すると、shallow_copy_null.cppの例のようにポインタを使うことなく実体の移動を記述できます。ここからは、このムーブを掘り下げていきましょう。

左辺値と右辺値[C++11]

 ムーブを語るにおいて、最初に押さえておきたいのが、C++ 11より導入された左辺値(lvalue)、右辺値(rvalue)という考え方です。字面だけ見ると、二項演算子の両脇にある、左辺、右辺とその値というところでしょうか。代入操作を思い浮かべると分かりますが、左辺は代入によって値が置き換わってそのまま存在し続けますが、右辺は代入に使われたら用済みとなるケースが多いです(リテラル値や関数の戻り値など)。このようにC++ 11からは、スコープの範囲で生存する値(名前のある値)を左辺値、有効なのはその場限りですぐに破棄される値(名前のない値)を右辺値として区別することになりました。

int a = 100, b = 200;
a = 200;            // aは左辺値、200は右辺値
b;                  // 変数を単に置いても左辺値
300;                // リテラルを単に置いても右辺値
b = a;              // bは左辺値、aも左辺値だが右辺値に変換できる
a = a + 400;        // a + 400の計算結果は右辺値
a = func();         // 関数の戻り値は右辺値

 実際に演算子の左辺にあるか、右辺にあるかということは無関係で、値の性質を表現したものといえるでしょうか。今回は日本語になっていますが、字面のイメージと実際の意味が若干異なるので、これも難しいですね。

右辺値参照[C++11]

 値には左辺値と右辺値がある!では参照も同じじゃない?と思った方は鋭いです。そこで、「右辺値参照」です。右辺値参照は、従来の参照(第2回でちょっとだけ紹介しました)と区別するためにC++11で導入されました。では従来の参照はどうなったのかというと、「左辺値参照」と呼ばれるようになっています。従来の参照は左辺値の参照に使われるので、左辺値参照というわけですね。

 左辺値参照(すなわち従来の参照)を指定するときは、「int&」というように「型名+&」としていました。右辺値参照を指定するときは「int&&」というように「型名+&&」となります。「&&」と&が1個増えるわけですね。紛らわしいですが、論理積の演算子とは違いますからね。以下は左辺値参照と右辺値参照の記述例ですが、特に右辺値参照では右辺値200を変数rで参照できるという点に注目です。

int a = 100;        // 整数型変数の宣言
int& l = a;         // lはaの左辺値参照
int&& r = 200;      // rは200の右辺値参照
cout << l << endl;  // 実行結果:100
cout << r << endl;  // 実行結果:200

 これらを引数の型に指定することで、参照の種類によって異なるオーバロードを定義することができます。これはすなわち次のコンストラクタと代入演算子の使い分けに結びつきます。ちなみに、右辺値参照でも左辺値参照でも、値をType&のように参照型とすることを「値をType&に束縛する」「拘束する」(ともにrestriction)というように表現しているようです。これもちょっとどころかかなり理解が難しいですよね。しかし、上の例のように右辺値200が後もrで参照できることから、「200がrに束縛される」という感じが少しは伝わるでしょうか。

ムーブコンストラクタとムーブ代入演算子[C++11]

 クラスのコピーでは、初期化時にはコピーコンストラクタが、代入時にはコピー代入演算子が用いられます。コピーを許すクラスでは、基本的にこの2つが実装されており、インスタンスの生成時や代入時にコピーが実行されることになっています(これらが実装されていないクラスでは、基本的にコピーはできません)。

 これまで見てきた通り、ムーブしたい場合はコピーと同じタイミングと方法で行います。そこでC++11では、ムーブコンストラクタとムーブ代入演算子というものが使えるようになりました。これらが実装されたクラスは、ムーブをサポートします。

 コピーとムーブをどう区別するのか?ということですが、これには上記の右辺値参照が活躍します。コンストラクタの引数や代入演算子の右辺が右辺値参照で解決できる場合には、それはムーブできる(すなわち値は捨てても良い、もう使わない)と判断してムーブコンストラクタおよびムーブ代入演算子が呼び出されます。

 以下のリストは、クラスにおけるコピーとムーブのコンストラクタと、代入演算子の定義例です。

リスト ctor_opr.cpp
class MyClass {
    public:
        // コンストラクタ
        MyClass(const MyClass &source);   // コピー
        MyClass(MyClass &&source);        // ムーブ

        // 代入演算子
        MyClass& operator=(const MyClass &source);    // コピー
        MyClass& operator=(MyClass &&source);         // ムーブ
};

 コピーコンストラクタにおいては引数がconst修飾されていますが、ムーブコンストラクタでは引数の値が書き換えられることもあるのでconst修飾されません(あとで紹介しますが、スマートポインタはその典型的なケースです)。シャローコピーの例で示したように、ムーブ後にムーブ元のポインタをnullptrで置き換えるケースが相当します。代入演算子も同様です。

右辺値へのキャスト[C++11]

 ムーブを試してみましょう。ディープコピーを、ムーブで書き直してみたのが以下のリストです。

リスト move_cast.cpp
vector<int> v(1000);
auto w = vector<int>(std::move(v));     // vを右辺値化する
//v[0] = 123;                           // vは無効なので実行時にエラーとなる
w[0] = 456; 
cout << w[0] << endl;                   // 実行結果:456

 wの初期化で、コンストラクタの引数にvを直接与えるのではなく、std::move関数の戻り値を与えています。move関数は、左辺値を右辺値にキャストします。これにより、コンパイラはvが右辺値であると判断して、vector<T>クラスのムーブコンストラクタを呼び出します。ムーブコンストラクタの呼び出しで結果としてvは使えなくなる(nullptrが入る)ので、その後は代入や参照などの一切の操作ができなくなります(そのようなコードを書くことはできますが、正常に動きません!)。move関数の呼び出しは、これを分かりやすい形でプログラマに伝えられるということです。

 なお、move関数はその名前に反して右辺値にキャストするだけで、ムーブ自体は行いません。戻り値をコンストラクタや代入演算子などに与えない限りは何も起きません。

次のページ
スマートポインタとムーブセマンティクス

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
Modern C++入門連載記事一覧

もっと読む

この記事の著者

WINGSプロジェクト 山内 直(WINGSプロジェクト ヤマウチ ナオ)

WINGSプロジェクトについて> 有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティ(代表 山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手がける。2018年11月時点での登録メンバは55名で、現在も執筆メンバを募集中。興味のある方は、どしどし応募頂きたい。著書記事多数。 RSS Twitter: @yyamada(公式)、@yyamada/wings(メンバーリスト) Facebook <個人紹介> WINGSプロジェクト所属のテクニカルライター。出版社を経てフリーランスとして独立。ライター、エディター、デベロッパー、講師業に従事。屋号は「たまデジ。」。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

山田 祥寛(ヤマダ ヨシヒロ)

静岡県榛原町生まれ。一橋大学経済学部卒業後、NECにてシステム企画業務に携わるが、2003年4月に念願かなってフリーライターに転身。Microsoft MVP for Visual Studio and Development Technologies。執筆コミュニティ「WINGSプロジェクト」代表。主な著書に「独習シリーズ(Java・C#・Python・PHP・Ruby・JSP&サーブレットなど)」「速習シリーズ(ASP.NET Core・Vue.js・React・TypeScript・ECMAScript、Laravelなど)」「改訂3版JavaScript本格入門」「これからはじめるReact実践入門」「はじめてのAndroidアプリ開発 Kotlin編 」他、著書多数

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/18574 2023/11/06 11:00

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング