SHOEISHA iD

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

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

Modern C++入門

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

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

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

 本連載では、Modern C++と称されるC++について、Modern C++らしい言語仕様をピックアップし紹介していきます。第4回は、Modern C++のキモとも言えるムーブセマンティクスを紹介します。オブジェクトのコピーが不要な場合には移動で済ませて、メモリ領域の有効利用と実行効率の向上に寄与するのがムーブセマンティクスです。対となる概念のコピーとともにムーブセマンティクスを理解します。

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

はじめに

 C言語から派生したオブジェクト指向プログラミング言語であるC++は、21世紀に入ってまったく別物とも言えるプログラミング言語に成長していきました。それは、Modern C++と称されています。1990年代にC++を触っていたプログラマが現在の仕様を知れば、隔世感に苛まれるのではないでしょうか。本連載では、かつてはC++をたしなんでいたという方、今からC++言語を始めるという方に向けて、Modern C++らしい言語仕様をピックアップし紹介していくことで、今のC++言語の姿を理解していただきます。

対象読者

  • かつてはC++をたしなんでいたという方
  • 今からC++言語を始めるという方
  • モダンなプログラミング言語のパラダイムに興味のある方

必要な環境

 本記事のサンプルコードは、以下の環境で動作を確認しています。

  • macOS Sonoma/Windows 11
    • Xcode Command Line Tools 2395
    • MinGW GCC 9.2.0

ムーブセマンティクスって何よ?

 20世紀のC++プログラマ(筆者)が、今(21世紀)のC++を知って驚く連載の第4回です。今回は、ムーブセマンティクスです。またまた、よく分からない言葉が出てきましたね。最近の、モダンを自称するプログラミング言語が難しく感じるのって、こういった言葉の問題があるんじゃないかと思ってしまいます。個人的な感覚では、オブジェクト指向なんてものが出てきてからこんな感じになったんじゃないかと思ってます。

 日本語にしにくいものはそのままカタカナ語として使うので、それがそもそも分かりにくいということもあります。そして、日本語にできてしまえばそれをそのまま使うことで、却って分かりにくくなるということも起きがちです。

 今回は、そのような事象との闘いと思って読んでいただければ幸いです(脱稿後、筆者の白髪の面積が少し増えたようです)。

[NOTE]C++プログラムのコンパイル

 以降のサンプルは、基本的に「gcc -o 出力先ファイル名 ソースファイル名」でコンパイルできますが、環境によってはライブラリやC++バージョンの指定が必要になることがあります。コンパイルやリンクでエラーが発生するときには、以下のようにライブラリの指定(-lstdc++)、C++バージョンの指定(-std=c++11)の追加を検討してください。

% gcc -o sample sample.cpp -lstdc++ -std=c++11	// c++14, c++17, c++20など

ムーブセマンティクス=移動意味論?

 まずは日本語を何とか当てはめてみるというところから、はじめてみましょう。ムーブ=移動、セマンティクス=意味論といったところなので、「移動意味論」でしょうか。これじゃ何のことか分かりませんね。少しアレンジして「移動に意味を持たせる」「移動という考え方」といったところでしょうか。どうやら、C++の世界に「移動」という意味を持ち込んだのがムーブセマンティクスと言えそうです。じゃあ、「移動」って何?何を移動するの?ということになりますね。

 C++の世界での移動とは、オブジェクトの移動を指します。オブジェクトの場所をaからbに移動する、そんな感じです。それにどういう意味があるのか、疑問が湧きますね。以降は、多くのドキュメントに倣って「移動」を「ムーブ」とそのまま称して、ムーブの意味を探っていきましょう。

C/C++の基本はコピー(複製)

 ところで、ムーブに対する概念といえばコピー(複製)です。ファイルのコピー、ファイルの移動というように、コンピュータで何かするときには対になって現れます。あまり意識することはないと思いますが、C/C++の初期化や代入の基本はコピー(複製)です。変数(インスタンス)の代入しかり、初期化子を伴う変数(インスタンス)の初期化も、基本的に「コピー」となります。ええ?本当?という人(筆者です)のために、サンプルで確かめてみましょう。変数(インスタンス)を宣言し、それを使って別の変数(インスタンス)を初期化するというものです(#includeやnamespaceはスペースの都合で割愛しているので、全体は配布サンプルを参照してください)。

リスト deep_copy.cpp
// 整数型変数xをyにコピーしてそれぞれ書き換える
int x = 100;
auto y = x;
y = 200;
cout << x << " " << y << endl;	// 実行結果:100 200

// 1000要素のベクターvをwにコピーして先頭要素のみ書き換える
vector<int> v(1000);
auto w = v;
v[0] = 123;
w[0] = 456; 
cout << v[0] << " " << w[0] << endl;	// 実行結果:123 456

 整数型変数の動きは通常の感覚でしょう。ベクターの方は、これはクラスのインスタンスだからコピーは参照だけとなるのでは?と(ちょっとJavaとかをかじった)筆者なら思うのですが、実際はコピーとなり、ベクターの先頭要素をそれぞれ書き換えてもそれは独立したものとなります。ちなみに、これらは「コピー構築」と呼ばれます。単なる代入なら「コピー代入」です。

 ちょっと深入りしますが、vector<T>クラスのインスタンスは、メンバフィールドとは別に全要素を格納するためのメモリ領域を別に保持しています。メンバフィールドをコピーするだけならコピーの実行コストは一定ですが、要素をコピーすると要素数に応じた実行コストとなります。このように、要素数によってはコピーに大きなコストがかかってくるのが注意点と言えます。

 このようなコピーは、「ディープコピー」と呼ばれます。変数がクラスのインスタンスである場合も、ベクターのように個々の要素まで「深く」コピーするからディープコピーというわけですね。このディープコピー、便利なようでいて、振る舞いをよく理解していないとトラブルや性能劣化の元なのですが、それについては後に譲ります。

ディープコピーとシャローコピー

 ディープコピーに対するシャローコピーというものもあります。ディープに対して「浅い」コピーだからシャローコピーというわけですが、これは、実行効率を考えるC/C++プログラマなら普通にやってきたことで、特別な言葉がくっついているだけと思ってます。

リスト shallow_copy.cpp
// 整数型ポインタ変数xをyにシャローコピーしてそれぞれ書き換える
auto* x = new int(100);
auto* y = x;
*y = 200;
cout << *x << " " << *y << endl;	// 実行結果:200 200

// 1000要素のベクターvをwにシャローコピーして先頭要素のみ書き換える
auto* v = new vector<int>(1000);
auto* w = v;
(*v)[0] = 123;
(*w)[0] = 456; 
cout << (*v)[0] << " " << (*w)[0] << endl;	// 実行結果:456 456

 丸ごとコピーするのではなく、場所(ポインタ)のみをコピーするのがシャローコピーと言えます。ここでは構築を示しましたが、関数の引数に与えるときも同じです。メモリの有効利用に敏感なC/C++プログラマなら、こういった書き方の方が自然かも知れませんね。

 シャローコピーは、ポインタを利用することでメモリのコピーを極力減らし、メモリの節約や実行速度の向上を図れます。ただ、複数のポインタ間で同じメモリ領域を共有していることを常に意識しておかないと、意図しない書き換えや解放が起きてしまうという危険性もあります。第3回でポインタの厄介さを紹介しましたが、こういった負担をプログラマに課すことからも、やっぱりポインタは厄介なんだと思いますね。

シャローコピーとポインタの無効化

 シャローコピーを紹介しましたが、実はこれがムーブセマンティクスの考え方のベースだったりします。shallow_copy.cppにちょっとコードを追加して書き換えたものが、以下のリストです。

リスト shallow_copy_null.cpp
auto* x = new int(100);
auto* y = x;		// yにシャローコピーしてxは用済みとしてマークする
x = nullptr;
*y = 200;
cout << *y << endl;	// *xは使えないので*yのみ表示、実行結果:200

auto* v = new vector<int>(1000);
auto* w = v;		// wにシャローコピーしてvは用済みとしてマークする
v = nullptr;
(*w)[0] = 456; 
cout << (*w)[0] << endl;	// *vは使えないので*wのみ表示、実行結果:456

 追加したコードは、シャローコピーのソースとなったポインタにnullptrを代入するというもので、ソースはもう用済みとしてポインタを無効化していることになります(別の回で取り上げますが、nullptrはヌルポインタを言語仕様で定めたものです。#defineしたNULLなどとは異なります)。これによって、シャローコピーの宛先となった変数のみがポインタとして有効となるので、意図しない書き換えや解放を「ある程度」防止できます。

会員登録無料すると、続きをお読みいただけます

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

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

メールバックナンバー

次のページ
ムーブで何ができる?

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

  • 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」など、さまざまなカンファレンスを企画・運営しています。

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

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

メールバックナンバー

アクセスランキング

アクセスランキング