yohhoyの日記(別館)

もうちょい長めの技術的メモをしていきたい日記

本当は怖くないムーブセマンティクス

この記事はC++ Advent Calendar 2012の15日目にエントリしています。
内容はC++11「ムーブセマンティクス」の入門記事となっています。

f:id:yohhoy:20121208022541j:plain
もくじ

  1. ムーブセマンティクス再考
    1. シンタックス vs. セマンティクス
    2. コピー vs. ムーブ
    3. ムーブのもつ2つの意味
    4. C++11のムーブセマンティクス対応
  2. auto_ptrからunique_ptrへ
    1. auto_ptrの暗い過去
    2. unique_ptrへの移行
    3. std::moveの役割
  3. ムーブセマンティクスを使おう
    1. C++11標準ライブラリとムーブ
    2. ムーブ"後"の中身は?
    3. ムーブを利用して関数を書く

(本文のみ約9500字)

まえがき

To move or not to move: that is the question.
  ― Bjarne Stroustrup, 2010(改)*1

プログラミング言語C++の新しい国際標準規格*2、いわゆるC++11が2011年9月に正式発行されてから1年ちょっとが経ちました。このC++11の新機能として、よく「ムーブセマンティクス(move semantics)」と「右辺値参照(rvalue reference)」がセットで紹介されます。Web上でも多くの入門・解説記事が見つかりますが、この2つがどんなものか把握できたでしょうか?実際のところ、「ムーブセマンティクス」はそれほど複雑なモノではありません。ですが、新しい「右辺値参照」や以前からの「(左辺値)参照」などの見慣れない用語を使って説明されると、最初のハードルが高く感じられるかもしれません。少なくとも私自身はそうでした…

そこで本記事のアプローチは:右辺値参照」の解説を完全に省いてしまい、ムーブセマンティクス」の解説と、クラスライブラリ利用者視点でのムーブ利用方法のみ説明していこうと思います。

対象とするのは、C++標準ライブラリ提供の“ムーブセマンティクスに対応した既製クラスを利用した関数の記述”までとし、“自作クラスをムーブセマンティクスに対応させる”などは範囲外となります。後者ではどうしても「右辺値参照」の説明が必要になりますし、そもそも利用者の観点から「ムーブセマンティクス」を理解しないことには、クラスの提供者になれるはずがありません。

おことわり:本記事では、ムーブセマンティクスと強い関係にある「右辺値参照」の説明を省いたため、正確性に欠ける表現となっている箇所もあります。なるべく脚注にて補記したつもりですが、その点は予めご了承ください。また「右辺値参照」の詳細については、より高度な解説記事を探してみて下さい。



1. ムーブセマンティクス再考

「ムーブセマンティクス」は、“ムーブ”+“セマンティクス”という2つの単語からなる用語です。まずは各単語の意味について考え、そこから「ムーブセマンティクス」を説明します。続いて、C++11の新機能との関係を整理しましょう。

1.1. シンタックス vs. セマンティクス

「ムーブセマンティクス」の後半部分、セマンティクス(semantics; 意味論)とは何でしょう?これと対比される単語、シンタックス(syntax; 構文)は?プログラミング言語の世界では、それぞれ次のように定義されます。

シンタックス(syntax)
目的の動作をするプログラムを、ソースコードとして具体的にどのように記述するかというルール。
セマンティクス(semantics)
ソースコードとして表現されるプログラムが、概念的にどのような意味を持っているかというルール。

セマンティクスの方は抽象的で少々難解ですが、もっと単純化すると次のようになります。

プログラミング言語は、プログラムで実現したい機能(セマンティクス)をソースコード上に表現するために、どのようにソースコードを書けばいいのか(シンタックス)を定める、ルールの集合体であるとも解釈できます。*3

簡単な例を挙げてみましょう。C++言語では、“数値加算”や“変数への代入”というセマンティクスを表現するため、記号 += を用いたシンタックスが提供されます。

// 数値1と2を足し算する。その結果を変数tへ代入する。
t = 1 + 2;
// 変数tの数値を取り出し、変数uへ代入する。
u = t;

…あまりに単純過ぎて小石でも投げつけられそうですが、この2つを区別することは非常に重要です。先の例ではシンタックスとセマンティクスが1:1で対応していますが、実はそうではない例も存在します。C言語を祖先にもつC++言語では、配列の要素への添え字アクセスがそれです。

// 配列aから4番目の要素を取り出し、変数bへ代入する。
b = a[4];
// 同
b = *(a + 4);

ここでは同じセマンティクスに対して、2種類の異なるシンタックスが使えます。一般的には配列要素アクセスは前者で記述しますが、実はこれ、シンタックス・シュガー(syntax sugar; 糖衣構文)の一種です。本来、C言語での配列要素アクセスは後者 *(a + 4) で十分なのです。しかし、これではソースコードから意図をくみ取るのが困難なため(ですよね?)、前者 a[4] というシンタックスを別途用意し、“配列に添え字で要素アクセスする”というセマンティクスを読み取り易くしています。

まずは、シンタックスとセマンティクスの違いを説明しました。また両者の関係は必ずしも1:1でない、という点も押さえておいてください。

1.2. コピー vs. ムーブ

あとは「ムーブセマンティクス」の前半部分、ムーブ(move; 移動)についてです。ムーブと対比されるのはコピー(copy; 複製)です。まぁ、この2つの違いは丁寧に説明しなくてもイメージを掴めるでしょう。ここでは変数tから変数uへ、その中身(文字列)をコピー/ムーブするイメージイラスト1枚でお茶を濁します。
f:id:yohhoy:20121210215421p:plain

ところで、C++言語における変数は“値のセマンティクス(value semantics)”を持つと言われます。ムーブセマンティクスの説明も途中なのに、新しい類似語を出すなんてと怒られそうですが、お話はちゃんと繋がっていますから。

さて、この“値のセマンティクス”を一言で表すと、“int型と同様に振る舞うこと”といえます。例えば文字列型std::stringでは、代入によって値(文字列)がコピーされ、別々の変数の中身はそれぞれで保持されます。そのため変数s2の中身を書き換えても、別の変数s1の中身には影響がありません。さらに標準コンテナstd::vectorなど、その中身として複数要素を保持するような型であっても同じことが言えます。*4

int i1 = 1;
int i2 = 2;
i1 = i2;  // i1==i2==2
i2 = 42;  // i1==2 && i2==42

std::string s1 = "apple";
std::string s2 = "banana";
s1 = s2;  // s1==s2=="banana"
s2 = "";  // s1=="banana" && s2==""

std::vector<int> v1;
std::vector<int> v2 = {1, 2, 3, 4};
v1 = v2;      // v1==v2=={1, 2, 3, 4}
v2[0] = 100;  // v1=={1, 2, 3, 4} && v2=={100, 2, 3, 4}

何を今更こんな当然の事を…と思いました?それは“C++言語の変数は値のセマンティクスを持つ”という概念モデルを、すでに認識しているからと言えます。

改めて先程のシンタックス/セマンティクスの関係で説明すると、代入シンタックス u = t は、変数tの中身をuへコピーするというセマンティクスを持ちます。ちなみに、ご存知の通り、文字列やコンテナのコピー処理にはそれなりの実行時コストがかかります。コピー元+コピー先分で2倍のメモリ領域が必要となりますし、コンテナの場合はその全要素値について個々にコピーが必要となるからですね。

1.3. ムーブのもつ2つの意味

先の“値のセマンティクス”を実現するには、本質的にコピーが必要でした。しかし実際のC++プログラムでは、必ずしもコピーしなくても十分なケースがあります。

std::vector<int> twice_vector(std::vector<int>);  // 全要素値を2倍して返す関数

std::vector<int> v = { /*大量の要素*/ };
std::vector<int> w = twice_vector( v ); // 関数へ変数vの中身を渡したい
// これ以後は変数vを使わない

この変数vの中身は、関数twice_vectorへ渡した後はもう必要とされません。なのに、関数呼び出し部分では必ずコピーが発生しています。C/C++は実行時コスト最小化のためなら、自分の足を撃ち抜くリスクすら厭わないプログラミング言語のはず。こんなことで良いのでしょうか!?

まぁ、この程度の例だと“参照型(std::vector<int>&)使え”で一蹴されるでしょうが、ここはムーブの出番でもあります。実行時コストが高くつく変数の中身のコピーを避けて、変数の中身を移動(ムーブ)してしまえば実行時コストを抑えられる、まさにそういうケースです。つまり、ここではムーブを「最適化されたコピー」と考えています。仮にここをコピーのままとしても、実行時コストが無駄にかかるだけで、プログラムの動作としては何ら影響がありません。*5

逆に、移動(ムーブ)でないとプログラムの意味が変わって/壊れてしまうケースもあります。例えばBoost.Smart Pointersライブラリ提供のスマートポインタboost::unique_ptrでは、“あるオブジェクトを指すunique_ptr変数はただ1つ”と決められています。仮にunique_ptr変数をコピーしてしまうと、このクラスの決め事(不変条件)に違反するため、この手のクラスではコピーが禁止されています。一方でムーブならば、変数pの中身を変数qへ移し、それと同時に元の変数pは何も指さなくすることで、クラスの不変条件を守ることができます。ここではムーブを「所有権の移動」と考えています。

boost::unique_ptr<int> p( new int(42) );
boost::unique_ptr<int> q = p;  // コピー禁止!(コンパイルエラーになる)

boost::unique_ptr<int> q ← p;  // ムーブならOK('←'はムーブを表すと仮定)
assert(*q==42 && p.get()==NULL);

というわけで、移動(ムーブ)という操作がもつ2つの意味(セマンティクス)、すなわち「ムーブセマンティクス」の異なる側面「最適化されたコピー」「所有権の移動」を整理しました。以降しばらくは、本質的にムーブを必要とする後者を例にあげて説明を続けます。

1.4. C++11のムーブセマンティクス対応

さて、ここまでの説明から“「所有権の移動」なら普通のポインタ(T*)でも出来るんじゃない?”と疑問がわくかもしれません。

int *p = new int(42);  // ポインタpが値42を所有する
int *q;

// pからqへ所有権を移動する
q = p;
p = NULL;
assert(*q==42 && p==NULL);

はい。正解です!変数pからqへの所有権の移動は、ポインタのコピー+移動元のNULLクリアの組合せでもちゃんと実現できます。つまり「ムーブセマンティクス」という考え方そのものは、以前からある考え方であり、かつ実装パターン(イディオム)に過ぎないのです。では、なぜC++11の新機能として「ムーブセマンティクス」が紹介されるのでしょうか。

先ほどの“普通のポインタによる所有権の移動”にはちょっとした問題があります。1回のムーブを表現するのに、2つの独立した操作(代入2回)を必要とします。このため、プログラマ自身がポインタ操作に十分気を配らないと、簡単にムーブセマンティクスが破壊されてしまいます。結局のところ、このように実現するムーブセマンティクスは、プログラマが勝手に決めたローカルルールでしかありません。

// pからqへ所有権を移動する?
q = p;
// "p = NULL;"を忘れると... 意図せず pからqへのコピー になる

ここで仮の話として、ムーブセマンティクスを直接表現する架空のシンタックス y ← x を導入してみましょう。もしこのシンタックスがあれば、所有権の移動を表すコードはシンプルで意図が明確になり、ポインタ版のような人為ミスによるセマンティクス破壊も起きえません。

SmartPtr p( new int(42) );  // ポインタpが値42を所有する
SmartPtr q;

// pからqへ所有権を移動
q ← p;

この“←演算子”は架空の話でしたが、実際のC++11でも「ムーブセマンティクス」を(間接的に)表現する、新しいシンタックスが追加されました。この新しいシンタックスこそが、C++11の「右辺値参照」として紹介される言語機能なのです。シンタックス追加によってプログラミング言語の仕様を拡張することで、コンパイラからムーブセマンティクスを認識できるようになり、これによりコンパイル時の最適化といった場面で有利にはたらきます。(どちらかと言えば暗黙のムーブ対応において意義があると思うのですが、これは本記事の範囲を超えます。)

それでは、C++11での「ムーブセマンティクス」と「右辺値参照」の関係を整理しましょう。

  • 「ムーブセマンティクス」は以前からある、単なる実装パターン(イディオム)です。
  • C++11ではコンパイラから「ムーブセマンティクス」を認識できるよう機能拡張します。
  • この目的のために追加された新機能が「右辺値参照」というシンタックスです。



2. auto_ptrからunique_ptrへ

ここからは、C++標準ライブラリ提供のスマートポインタstd::auto_ptrstd::unique_ptrを用いて、直接的にC++11の「ムーブセマンティクス」を表現する、具体的なシンタックスを見ていきましょう。先ほどは、「右辺値参照」で“間接的に”ムーブセマンティクスを表現するとしましたが、ここで初めて std::move を使ったシンタックスが登場します。
(注:これ以後登場する"C++03"は、C++11発行前の国際標準規格であった過去のC++言語を指します。…現実には当面の間C++03が使われ続けるでしょうけど。)

2.1. auto_ptrの暗い過去

古き良きC++03標準ライブラリのなかに、1つだけ「ムーブセマンティクス」をサポートしたクラスがありました。それがstd::auto_ptrです。

std::auto_ptr<int> p( int new(42) );  // ポインタpが値42を所有する
std::auto_ptr<int> q;

// pからqへ所有権を移動(ムーブ)
q = p;
assert(*q==42 && p.get()==NULL);

あまり積極的に利用されてこなかった気もしますし、std::auto_ptrは標準コンテナに格納できないといった問題もあって、C++11標準ライブラリではあえなく非推奨(deprecated)となりました。

さて、ここまでのシンタックス/セマンティクスの説明を踏まえ、もう一度std::auto_ptrを使ったコードをみると、何かおかしな事に気づきませんか?

std::string s1 = "apple";
std::string s2;

s2 = s1;  // s1からs2へ"コピー"
// s1==s2=="apple"

std::auto_ptr<int> p1( int new(42) );
std::auto_ptr<int> p2;

p2 = p1;  // p1からp2へ"ムーブ"
// p1.get()==NULL && *p2==42

どちらも同じ代入シンタックス u = t にも関わらず、“値のセマンティクス”を持つ普通の型std::stringではコピーが行われるのに対して、std::auto_ptrではムーブが行われています。もちろん、この動作そのものはstd::auto_ptrクラスの仕様通りなのですが、結果として同じシンタックスを用いているのに、std::auto_ptrだけが異なるセマンティクスを持つという状態になっています。

2.2. unique_ptrへの移行

C++言語の変数では“値のセマンティクス”が基本となるため、代入シンタックス u = t は値のコピーというセマンティクスを持つべきですし、C++プログラマにもこれが共通認識としてあるはずです。結局のところ、std::auto_ptrクラスが非推奨となった原因は、シンタックスから期待されるセマンティクスとの不一致、それによる使いづらさ・間違いやすさから来ています。

引退したstd::auto_ptrの代わりに、C++11からはstd::unique_ptrが追加されます。セマンティクスの観点からみると、両クラスともコピーを許容せず、所有権の移動のみが可能なスマートポインタという意味では同一です。この2クラスの差異は、ムーブセマンティクスを実現するシンタックスの観点で明らかになります。*6

// C++03時代 auto_ptr
std::auto_ptr<int> p1( int new(42) );
std::auto_ptr<int> p2;
p2 = p1;  // 代入シンタックスを用いて"ムーブ"を表す(混乱のもと)

// C++11以降 unique_ptr
std::unique_ptr<int> p1( int new(42) );
std::unique_ptr<int> p2;
p2 = p1;  // 代入シンタックスは"コピー"を表す;コピー不可のためコンパイルエラー

std::unique_ptrで普通に代入シンタックス u = t を用いると、セマンティクスとしては所有権の複製(コピー)を表します。そしてこのスマートポインタではコピーが禁止されているため、この書き方はコンパイルエラーとなるのです。もう一度std::auto_ptrと比較してみてください。

2.3. std::moveの役割

ここで初めて、「ムーブセマンティクス」を直接表現するシンタックスが必要となります。C++11では代入演算子 =std::move を組み合わせたものがムーブ代入シンタックス u = std::move(t) となり、これによってムーブセマンティクスを表現します。*7

std::unique_ptr<int> p1( int new(42) );
std::unique_ptr<int> p2;

// p1からp2へコピー
p2 = p1;             // (コピー)代入シンタックス
// コピー不可のため上記はコンパイルエラー!

// p1からp2へ所有権を移動
p2 = std::move(p1);  // ムーブ代入シンタックス
// p1.get()==NULL && *p2==42

コピー/ムーブの区別を明確にするため、これまで単に代入シンタックスと呼んでいたものは、コピー代入シンタックス u = t としておきましょう。

一旦シンタックスの話を整理するため、まずは昔ながらのコピーから考えます。C++言語で値の複製(コピー)というセマンティクスに利用するのは、「コピーコンストラクタ(copy constructor)」と「コピー代入演算子(copy assignment operator)」シンタックスの2種類です。(ここまでは代入演算子のみ取り上げていました)

T t1, t2, u2;  // Tという適当な型を仮定

T u1( t1 );  // コピーコンストラクタによる"コピー"
u2 = t2;     // コピー代入演算子による"コピー"

対応するムーブ版は、それぞれ「ムーブコンストラクタ(move constructor)」と「ムーブ代入演算子(move assignment operator)」シンタックスとなります。どちらも std::move が間にはさまること以外は、コピー版と同じ構造をしています。*8

T u1( std::move(t1) );  // ムーブコンストラクタによる"ムーブ"
u2 = std::move(t2);     // ムーブ代入演算子による"ムーブ"

最後に、コピー/ムーブに対応するシンタックスを下表にまとめます。ね、簡単でしょ?

コピー ムーブ
コンストラクタ T u( t ) T u( std::move(t) )
代入演算子 u = t u = std::move(t)


わき道小話1
ところで std::move の正体は何でしょうか?実はコレ、C++標準ライブラリ提供の単なる関数テンプレートです。標準ヘッダ<utility>で提供されるため、前掲サンプルコード群では正確には同ヘッダをincludeする必要がありました。

#include <utility>

本記事の説明範囲では、“コンストラクタ/代入演算子std::move組み合わせによってムーブセマンティクスを表現する”より詳細化できないため、これ以上は深追いしません。1つだけ注記するなら、std::move関数を単体で使っても“何も起こらない”という事だけ覚えておいてください。

T t, u;
std::move(t);      // 何も起こらない...
u = std::move(t);  // tからuへムーブ



3. ムーブセマンティクスを使おう

続いて、ムーブセマンティクスに対応したクラスライブラリの利用方法、つまりライブラリ提供のある型(クラス)がムーブセマンティクスに対応するとき、そのクラスの使い方ついて説明していきます。ここではC++11標準ライブラリを例に取りあげます。なんと言っても“標準”ですから。

3.1. C++11標準ライブラリとムーブ

C++標準ライブラリは、文字列/標準コンテナ/スマートポインタなどのプログラム基礎部品として使えるクラス群を提供します(正確には“クラステンプレート”で提供されますが、簡単のためクラスで説明を続けます)。C++11標準ライブラリで提供されるクラス群は、基本的に全てムーブセマンティクスに対応しました。これはC++11で新規追加されたクラスstd::unique_ptrだけでなく、C++03時代から存在する馴染みのクラスstd::stringstd::vectorなども、C++11ではムーブセマンティクスに対応したという意味です。

std::string s1 = "apple";
std::string s2 = "banana";

s1 = std::move(s2);  // s2からs1へムーブ
// s1=="banana"

std::vector<int> v1;
std::vector<int> v2 = {1, 2, 3, 4};

v1 = std::move(v2);  // v2からv1へムーブ
// v1=={1, 2, 3, 4}

std::stringstd::vectorは“値のセマンティクス”をもつため、もちろん従来通りコピーすることもできます。言い換えると、これらのクラスではムーブセマンティクスの導入によって、ムーブを「最適化されたコピー」として利用できるようになりました。またコンテナクラスでは、“コンテナ全体のムーブ”と“コンテナ要素に対するムーブ”の両方が拡張されています。

std::string s1 = "apple";
std::string s2 = "banana";

std::vector<std::string> dict;

dict.push_back( s1 );             // s1からコンテナ内へ"コピー"して要素追加
// s1 == "apple"
dict.push_back( std::move(s2) );  // s2からコンテナ内へ"ムーブ"して要素追加
// (s2の状態は後述)

std::vector<std::string> v;
v = std::move(dict);  // コンテナ全体をdictからvへムーブ
// v[0]=="apple" && v[1]=="banana"

また、既に説明したとおりstd::unique_ptrはムーブを「所有権の移動」に利用します。さらに、C++03時代は単にコピーが禁止されていたI/Oストリームクラスstd::fstream, std::stringstreamなどでは、C++11以降はムーブによる「所有権の移動」が出来るようになりました。

とはいえ、C++標準ライブラリのあらゆるクラスでムーブ可能となった訳ではありません。中にはC++11で追加されたスレッド間排他制御ミューテックスstd::mutexのように、コピーもムーブも両方禁止というクラスも存在します。

まとめると、C++11標準ライブラリ提供のクラスは3種類に分類されます。

  • コピー・ムーブ両方が可能なクラス(ムーブは「最適化されたコピー」)
  • コピー禁止だがムーブのみ可能なクラス(ムーブは「所有権の移動」)
  • コピー・ムーブ両方が禁止されるクラス

わき道小話2
C++標準ライブラリのうち“値のセマンティクス”をもつクラスでは、「最適化されたコピー」としてのムーブに対応したと説明しました。ところが、クラスの内部実装によってはこれ以上最適化できない、というケースも存在します。このようなクラスはコピー/ムーブともに可能ではあるものの、コピー/ムーブで同程度の実行時コストが掛かってしまいます。

#include <array>
std::array<int, 100> a1 = { /*...*/ };
std::array<int, 100> a2 = { /*...*/ };

a1 = a2;             // a2からa1へ"コピー"
a1 = std::move(a2);  // a2からa1へ"ムーブ"? →実際は"コピー"

この手のクラスでは、コピーコンストラクタ/代入演算子だけが提供され、ムーブコンストラクタ/代入演算子は提供されません。ただ、この場合にもムーブ代入シンタックス u = std::move(t) はちゃんとコンパイルできます。しかし実際にはコピー代入演算子が呼びだされ、結果としてコピーの処理が行われるという動きになります(詳細は本記事の範囲を超えます)。C++11標準ライブラリでは、少なくとも固定長配列クラスstd::array複素数クラスstd::complexがこのような対応を取っています。

3.2. ムーブ"後"の中身は?

さて、ここまでの説明で敢えて避けてきた話題があります。ムーブされた"後"の変数には何が入っているのでしょう?それらしい答えとしては“空っぽ”になる?

std::string t = "xmas", u;

u = std::move(t);  // tからuへムーブ
// u=="xmas" …ではtの中身は?

ええと、この質問に答えるのは少々厄介です。C++11標準ライブラリでは、ムーブ後は基本的に“有効だが規定されない状態(valid but unspecified state)”になると定めています。何を言っているのか意味不明ですが、この段階では一言も“ムーブ後は空っぽになる”と保証していないことに注意してください。

例えばstd::stringでは、まさにこの“有効だが規定されない状態(valid but unspecified state)”になります。簡単な表現では「中に何がはいっているか知りません(未規定)」だけど「変数への操作は出来るよ(有効)」という状態のことです。

std::string t = "xmas", u;
u = std::move(t);  // tからuへムーブ

// OK: t = "X";  再代入は問題無くできる
// OK: t.size()  サイズ問合せはできる …がどんな値かは不明
// NG: t[2];     中身がどうなっているか分からないからダメ(空っぽかもしれない)

結局のところムーブ後の中身がどうなるか規定されないケースでは、事実上“その変数への再代入”しか使い道がないと思います。

ただし例外事項として、ムーブが「所有権の移動」を表すクラスでは“ムーブ後の変数が確かに所有権を持たない”必要があり、この場合はムーブ後の変数が空っぽであると保証されます。(詳細はこのあたりを参考にしてください。)

std::unique_ptr<int> p1( int new(42) );
std::unique_ptr<int> p2;

p2 = std::move(p1);  // p1からp2へ所有権を移動(ムーブ)
// p1は所有権を持たない p1.get()==NULL ことを保証

ムーブ後の状態について一通り説明しましたが、普通は気にかける必要はありません。だって、その“変数”が不要になるからムーブで中身を取出すんでしょ?

3.3. ムーブを利用して関数を書く

最後に、自作の関数でのムーブセマンティクス利用方法について説明します(ここでは関数テンプレートではなく、普通の関数を対象とします*9)。ここまでは隣同士にある変数間でムーブしていましたが、実際のプログラムで利用する場面はまず無いでしょう。ムーブセマンティクスが本当に役立つのは、関数呼び出しをまたいで値をやり取りするケース、つまり (1)関数呼び出し時に引数へムーブで渡す、そして (2)関数から戻り値をムーブで返す の2パターンです。

いきなり答えを書いてしまうと、(1)と(2)両方でいわゆる値渡し(pass-by-value)」スタイルを使うのが正解となります。関数twice_vectorの (1)引数の型 と (2)戻り値の型 が、それぞれ普通のIntVecであることに注意して下さい。また (1)関数の呼び出し側 と (2)関数からのreturn文 の両方で std::move を使っていることにも着目してください。*10

// 全要素値を2倍して返す関数
typedef std::vector<int> IntVec;
IntVec twice_vector(IntVec a)
{
  for (auto& e : a)
    e *= 2;
  return std::move(a);  // (2)"ムーブ"で戻り値を返す
}

IntVec v = { /*...*/ };
IntVec w = twice_vector( std::move(v) );  // (1)"ムーブ"で引数へ値を渡す

C++03時代だと、この書き方は“コンテナ全体をコピーするなんて非効率極まりない”と四方八方からマサカリが飛んできますが、ムーブセマンティクス対応のC++11以降ではこれがベストになります。ちなみに、上記コードでstd::moveを消してしまえば、C++11であってもC++03時代と同じくコピーが行われます。
(引数の型に関する議論はここで簡単に比較しています。)

One more thing...

冒頭で「右辺値参照」については一切説明しないと言いましたが、ありがちな誤用例だけはアンチパターンとして紹介しておきたいと思います。
注意:右辺値参照型の使いどころを誤ると罠にハマるというお話です。関数の引数/戻り値では右辺値参照型を使えないという意味ではありません。

パターン1: 引数の型に右辺値参照型IntVec&&を誤用する。
関数呼び出し側ではコピーによる値渡しができなくなります。言い換えると、普通の“変数を引数として関数に渡す呼び出し”がコンパイルエラーになります。不便な関数仕様の出来上がり。

// 誤用パターン1
typedef std::vector<int> IntVec;
IntVec twice_vector(IntVec && a)  // 引数型 = IntVec&&
{
  for (auto& e : a)
    e *= 2;
  return std::move(a);
}

IntVec v1, v2, w1, w2;
w1 = twice_vector( v1 );             // NG: コピーのシンタックスで呼び出せない!
w2 = twice_vector( std::move(v2) );  // OK: ムーブのシンタックスのみ受け付ける

パターン2: 戻り値の型に右辺値参照型IntVec&&を誤用する。
ダメ!絶対!参照先の変数aが有効なのは、関数twice_vectorの中だけです。この変数への“参照”を返してしまうと、関数呼び出し側に戻ったときには既に参照先が存在しません。右辺値参照型もしょせんは“参照型”なのです。その実行結果は未定義動作となり、恐らくクラッシュなどを引き起こすでしょう。

// 誤用パターン2
typedef std::vector<int> IntVec;
IntVec && twice_vector(IntVec a)  // 戻り値型 = IntVec&&
{
  for (auto& e : a)
    e *= 2;
  return std::move(a);
  // ここで変数aの生存期間は終わり!
}

IntVec v1, v2, w1, w2;
w1 = twice_vector( v1 );             // NG: 未定義動作!
w2 = twice_vector( std::move(v2) );  // NG: 未定義動作!

既に見てきた通り、「右辺値参照」で「ムーブセマンティクス」を実現するという表現に嘘はありません。でもムーブを“利用する”だけなら、右辺値参照型を直接使う必要はないんです。くれぐれも誤用には気をつけて!
(本当に右辺値参照型が必要になるのは、ムーブに対応したクラスの自作や、関数テンプレートのテンプレート引数型で使うケースです。)



おわりに

以上で説明はおしまいです。おつかれさまでした。改めて要点を列挙しましょう:

  • 「ムーブセマンティクス」は昔からあるイディオムですが、C++11からはコンパイラもムーブセマンティクスを認識できるようになりました。
  • C++11標準ライブラリ提供のクラスは、「最適化されたコピー」または「所有権の移動」としてのムーブに対応しました。
  • コピー代入シンタックス u = t とムーブ代入シンタックス u = std::move(t) を区別し、用途に応じて使い分けましょう。
  • ムーブを利用するだけなら、「右辺値参照」を気にする必要はありません。関数の引数/戻り値では「値渡し」スタイルを使いましょう。

さて今回の「右辺値参照」なしで「ムーブセマンティクス」を説明する試み、いかがだったでしょうか?本記事が少しでも理解の助けになればと願います。

C++ Advent Calendar 2012 は 16日目 @fadis_ さんの記事へと続き(move to)ます。Happy Holidays!
f:id:yohhoy:20121208020855j:plain
flickr / scpgt

*1:ISO/IEC JTC1/SC22/WG21 N3174 paperより。

*2:正式名称は ISO/IEC 14882:2011 Information technology -- Programming languages -- C++ です。ちなみに、一つ前の国際標準だった ISO/IEC 14882:2003 のステータスはWithdrawn、つまり新しい標準規格C++11の改訂によって以前の標準規格C++03は取り下げられています。

*3:実際に、“標準化”されたプログラミング言語仕様書はこういった定義を行います。一方で、“コンパイラインタプリタなどの処理系実装ありき”で仕様化(?)されるプログラミング言語も存在します。ただし後者では別の処理系を実装するのが非常に困難となるため、言語仕様とその実装は区別して取り扱うべきだと思います。

*4:話を単純化するため、ここではポインタ型(T*)や参照型(T&, T&&)は無視します。C++言語の参照型は“値のセマンティクス”とは異なる“参照のセマンティクス(reference semantics)”を持ちます。一方で、ポインタ型はさまざまなセマンティクスをあわせ持っています。例えば、ポインタ型変数+さす先の値をセットで考えれば“参照のセマンティクス”を持つといえます。ただ、ポインタ型変数の中身(≒アドレス値)だけに着目すると“値のセマンティクス”とも言えます。本記事の文脈では、後者“値のセマンティクス”として扱います(後半のスマートポインタ例示が相当)。

*5:“最適化されたコピーとしてのムーブ”という考え方は、書籍Effective C++シリーズで有名なScott Meyers氏による記事Initial Thoughts on Effective C++11 "For Copyable Types, View Move as an Optimization of Copy"からの受け売りです。抄訳はこちら

*6:ここでの議論とは無関係なため、std::unique_ptrのカスタムデリータといった追加機能は無視します。

*7:ここでは簡単のため右辺値としてxvalueのみを想定し、prvalueからのムーブについては無視しています。

*8:本記事の範囲では std::moveありき で説明してしまいます。実際のシンタックス上では、コンストラクタ/代入演算子の引数の型が 左辺値参照型(T&)または右辺値参照型(T&&)のいずれか によってコピー/ムーブが識別されます(cv修飾は除去して考える)。

*9:関数テンプレートの引数では型推論が行われるため、「右辺値参照」なしではロクな説明ができません。よって割愛。

*10:実は今回の例では、(2)return文ではstd::moveを使わない方がベターです。C++コンパイラが正しく「暗黙のムーブ」対応していれば自動的にムーブされますし、さらに言うと通称“名前付き戻り値最適化(NRVO)”と呼ばれる仕組みによって、ムーブ処理さえ最適化(省略)できる可能性があります。本記事では「暗黙のムーブ」に言及しないため、常にstd::moveを明示指定することとします。

Boost.勉強会 #10 に参加しました

2012/7/28に開催されたイベント Boost.勉強会 #10 東京 にて、“C++ Transactional Memory言語拡張の紹介”というタイトルで20分ほど話す機会をいただきました。

質疑応答

下記は発表後に行われた質問と回答を並べたものです。改めて書き起こすと結構分量がありますね。

  • Q. コミットとキャンセルしか提供されず機能がミニマムセット過ぎる?(Haskeller曰く)リトライが無いと使い物にならないらしい。補足として、TBoost.STMにはリトライ機能が存在する。
  • A.ドラフト仕様Ver.1.1段階では指摘どおり。ドラフト仕様のAppendixに拡張機能の候補として上げられており、今後の改訂で追加される可能性はあるかも。
  • Q. gccのTM拡張はゼロオーバーヘッドなのか?TM拡張を有効にした場合、atomicトランザクションを利用しなくてもオーバーヘッドは無いのか?
  • A. 性能については分からない。現時点ではSTM実装となっており何らかのオーバーヘッドがあるかも。
  • Q. atomicトランザクションから操作する変数であるか否かを指定する構文が存在しないとすると、全ての変数がトランザクション操作されうると見なすのか?
  • A. atomicトランザクションをSTMでどう実装するのか想像つかなかった。HTMなら専用命令で変数アクセスを検知できるような気がする。
  • Q. HTMでも対象となるアドレス数(変数の個数)には制限があるのでは?
  • A. そう思う。
  • Q. トランザクション内ではint等のプリミティブしか扱えない?クラスは?
  • A. 使える。クラス操作はプリミティブ操作の集合へと分解されるため。
  • Q. トランザクション内でnew演算子も使える?
  • A. 仕様上は組み込みの演算子に限りnew/deleteを使えるとある。ただし処理系によっては使用不可と制限がかかるかもしれない。
  • Q. ロックとTMのパフォーマンス比較って何か公開されている?
  • A. 自分で計ったデータは無い。GCC公式ドキュメントにも実行速度は今後にご期待くださいとあるのみ。
  • Q. トランザクション内から整数しか例外送出できないのは超悲しい。ポインタ値を送出した場合にIsolationが壊れるのを防ぐためだろう。何とかかならないのか?
  • A. 何とかなる仕様を提案してね。
  • Q. atomicトランザクション内でファイルI/Oのような非可逆操作を行おうとすると、ビルドエラーとなるのか?
  • A. コンパイル時エラーとなる。transaction_safe/unsafe属性によりコンパイル時に判断可能な仕様設計となっている。
  • Q. 例えばC言語ライブラリを呼ぶなど判断できない場合はどうする?
  • A. 標準ライブラリが提供する安全でない関数には、処理系の責任でtransaction_unsafe属性を付けているはず。

感想とか

参加された方々の前提知識レベルをどこに置けばよいか分からず、発表時間の半分をTransactional Memoryそのものの説明に当てました。一方で共有メモリアーキテクチャを前提とすることや、マルチスレッドプログラミングの知識があることは無条件に仮定しています。もっとC++TM言語拡張仕様について説明するパターンでも良かったのかなと感じました。

発表内容の意図としては「C++言語仕様に対するTransactional Memory拡張“言語仕様”の紹介」でしたが、当然ながらSTM/HTM等でどのように実装・実現されるのかという質問も出ました。実性能が気になるというのは十分予測されたのですが、現時点では性能面における期待はできないと考え、発表内容は純粋に言語仕様のみを対象としました。

今回紹介したC++TM言語拡張が本当に次期C++へ導入されるか否かは分かりませんが、Transactional Memoryなどの並列・並行処理における抽象化機構に足を踏み入れるきっかけになれば幸いです。

UnVisualBasic 6.0

この記事は2001年頃に書いた文章をそのまま転記し、はてなブログ用に体裁を整えたものです。Visual Basic 6.0を前提としていますが、当時からネタ記事なので悪しからず。

プロジェクト一式はこちら: https://gist.github.com/yohhoy/3054269


まえがき

往々にしてGUI(Graphical User Interface)プログラミングではコード量が多くなり面倒なものですが、Microsoft社のVisualBasicを用いると僅かなコーディングで簡単・迅速にGUI構築することが可能になります。
しかし、VBの守備範囲を越えたことを実装する際には、どうしてもWin32API等に頼らざるを得ません。これを見越して(?)かVBには外部DLL関数を呼ぶ仕掛けが用意されています。この機能を使えばWindows上で可能なことは大抵実装できます。さらにVB5.0から導入された AddressOf演算子 を用いるとVB関数のアドレスが取得できます。つまりVBでコールバック関数が使用可能になったのです。この AddressOf演算子 の登場で、(実用上ほぼ)全てのAPI関数が利用できるようになったのです。

目的

AddressOf演算子 の登場でVBでもウインドウのサブクラス化を実現できるようになり、今までOCXやDLLに頼っていた機能を自前で実装できるようになりました。
ここまでVBで実現できるのなら、いっそVBの機能を使わず(基本制御構造は除く)にWin32APIのみでソフトを開発出来ないかと考えてみました。要するに、本来ならメニューから

[プロジェクト]→[フォームモジュールの追加]

とするだけで終わるところを、RegisterClass関数やCreateWindow関数等を用いて実装しようというのが目的です。

実装例1

とりあえず上記の目標に従って実装してみました。ただしAPI関数のDeclare宣言や型の定義、定数の定義は省いています。

Private m_hInstance As Long

Private Const WNDCLSNAME As String = "TrickyVbWndCls"
Private Const IDM_EXIT = 1

'
' Entry Point
'
Public Sub Main()
  Dim wc As WNDCLASS
  Dim hMenu As Long, hSubMenu As Long
  Dim hwnd As Long
  Dim message As MSG

  '初期化
  m_hInstance = GetModuleHandle(0)

  'ウインドウクラスを登録
  With wc
    .style = CS_VREDRAW Or CS_HREDRAW
    .lpfnWndProc = funcaddr(AddressOf WindowProc)
    .cbClsExtra = 0
    .cbWndExtra = 0
    .hinstance = m_hInstance
    .hIcon = LoadIcon(0, IDI_APPLICATION)
    .hCursor = LoadCursor(0, IDC_ARROW)
    .hbrBackground = COLOR_BTNFACE + 1
    .lpszMenuName = vbNullString
    .lpszClassName = WNDCLSNAME
  End With
  Call RegisterClass(wc)

  'メニュー作成
  hMenu = CreateMenu()
  hSubMenu = CreatePopupMenu()
  Call AppendMenu(hMenu, MF_STRING Or MF_POPUP, hSubMenu, "&File")
  Call AppendMenu(hSubMenu, MF_STRING, IDM_EXIT, "&Exit")

  'ウインドウ作成
  hwnd = CreateWindowEx(0, WNDCLSNAME, "tricky", _
                  WS_OVERLAPPEDWINDOW, _
                  CW_USEDEFAULT, CW_USEDEFAULT, _
                  CW_USEDEFAULT, CW_USEDEFAULT, _
                  0, hMenu, m_hInstance, 0)
  Call ShowWindow(hwnd, SW_SHOW)

  'メッセージループ
  Do While (GetMessage(message, 0, 0, 0))
    Call TranslateMessage(message)
    Call DispatchMessage(message)
  Loop

  'メニュー破棄
  Call DestroyMenu(hSubMenu)
  Call DestroyMenu(hMenu)

End Sub

'
' ウインドウプロシージャ
'
Private Function WindowProc(ByVal hwnd As Long, ByVal uMsg As Long, _
  ByVal wParam As Long, ByVal lParam As Long) As Long

  WindowProc = 0

  Select Case uMsg
  Case WM_CREATE
    '

  Case WM_COMMAND
    Select Case (wParam And &HFFFF)
    Case IDM_EXIT '[Exit]
      'ウインドウを閉じる
      Call SendMessage(hwnd, WM_CLOSE, 0, 0)
    End Select

  Case WM_DESTROY
    'アプリケーション終了
    Call PostQuitMessage(0)

  Case Else
    WindowProc = DefWindowProc(hwnd, uMsg, wParam, lParam)

  End Select

End Function

Private Function funcaddr(ByVal addr As Long) As Long
  funcaddr = addr
End Function

当然のことですが、基本制御構造の If や Do While や Select Case、Call ステートメント を使用しないというのは不可能ですね。
あと、メニューはリソースにしようと思ったのですがVBのリソースエディタでは無理でした。VC++でリソース(.res)だけ作成すればどうにかなったかも…
わざわざ funcaddr()関数を作っているのは、AddressOf演算子が返す値(Long型)を変数に直接代入できなかった為です。

実装例2

ちなみに実装例1と同等のプログラムを、VB本来の手法で作ってみます。

  • メニューから[プロジェクト]→[フォームモジュールの追加]
  • 新しく作ったフォームを右クリックして[メニューエディタ]
  • キャプション:&File 名前:mnuFile [次へ]をクリック
  • キャプション:&Exit 名前:mnuFileExit [⇒]をクリック
  • [OK]で閉じる
  • フォーム上のメニューから[File]→[Exit]

開いたコードエディタ上で

Private Sub mnuFileExit_Click()
    Unload Me
End Sub

以上。
コーディング量は "Unload Me" の9文字だけ!この際 "Unload" だけでも可!

結論

餅は餅屋

付録

普通にC言語とWin32SDKで作るとこんな感じになります。

#define STRICT
#include <windows.h>

/* マクロ定義 */
#define WNDCLSNAME "TrickyVbWndCls"
#define IDM_EXIT   1

/* 関数プロトタイプ宣言 */
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg,
  WPARAM wParam, LPARAM lParam);

/* Entry Point */
int WINAPI WinMain(
  HINSTANCE hInstance,
  HINSTANCE hPrevInstance,
  LPSTR lpCmdLine,
  int nCmdShow)
{
  WNDCLASS wc;
  HMENU hmenu, hsubmenu;
  HWND hwnd;
  MSG msg;

  /* ウインドウクラスを登録 */
  wc.style = CS_VREDRAW | CS_HREDRAW;
  wc.lpfnWndProc = WindowProc;
  wc.cbClsExtra = 0;
  wc.cbWndExtra = 0;
  wc.hInstance = hInstance;
  wc.hIcon = LoadIcon(0, IDI_APPLICATION);
  wc.hCursor = LoadCursor(0, IDC_ARROW);
  wc.hbrBackground = (HBRUSH)(COLOR_BTNFACE + 1);
  wc.lpszMenuName = NULL;
  wc.lpszClassName = WNDCLSNAME;
  RegisterClass(&wc);

  /* メニュー作成 */
  hmenu = CreateMenu();
  hsubmenu = CreatePopupMenu();
  AppendMenu(hmenu, MF_STRING|MF_POPUP, (UINT)hsubmenu, "&File");
  AppendMenu(hsubmenu, MF_STRING, IDM_EXIT, "&Exit");

  /* ウインドウ作成 */
  hwnd = CreateWindowEx(0, WNDCLSNAME, "tricky",
                        WS_OVERLAPPEDWINDOW,
                        CW_USEDEFAULT, CW_USEDEFAULT,
                        CW_USEDEFAULT, CW_USEDEFAULT,
                        0, hmenu, hInstance, 0);
  ShowWindow(hwnd, nCmdShow);

  /* メッセージループ */
  while (GetMessage(&msg, NULL, 0, 0)) {
     TranslateMessage(&msg);
     DispatchMessage(&msg);
  }

  /* メニュー破棄 */
  DestroyMenu(hsubmenu);
  DestroyMenu(hmenu);

  return 0;
}

/* ウインドウプロシージャ */
LRESULT CALLBACK WindowProc(
  HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  switch (uMsg) {
  case WM_CREATE:
    /* ... */
    break;

  case WM_COMMAND:
    switch (LOWORD(wParam)) {
    case IDM_EXIT: /* [Exit] */
       /* ウインドウを閉じる*/
       SendMessage(hwnd, WM_CLOSE, 0, 0);
       break;
    }
    break;

  case WM_DESTROY:
    /* アプリケーション終了 */
    PostQuitMessage(0);
    break;

  default:
    return DefWindowProc(hwnd, uMsg, wParam, lParam);
  }
  return 0;
}

OLE Drag & Dropを実装する

この記事は2001年頃に書いた文章をそのまま転記し、はてなブログ用に体裁を整えたものです。Windows2000+Win32 SDKC言語を前提としており、2012年現在では内容が陳腐化している箇所もあります。

(当時の)ソースコードはこちら: https://gist.github.com/3053385


目的

Windowsの便利な機能に、ドラッグ・アンド・ドロップ(以下D&D)によるファイルのコピー・移動・ショートカット作成があります。これに相当する機能を実装してみようというのが今回の目的です。
ちなみに開発環境は、Win32 SDKベースのC言語を想定しています。

非OLE Drag&Drop

Explorer等からのファイルD&Dを実装する際に、最も簡単なのはDragAcceptFiles()関数を用いる方法です。
ファイルのドロップを受け付けたいウインドウのウインドウハンドル(hwnd)とすると、

HWND hwnd = /* 対象ウインドウのHWND */;

DragAcceptFiles(hwnd, TRUE);

とすることで、ファイルのドロップを受け付けるようになります。
対象ウインドウにExplorerからファイルがD&Dされると、ウインドウプロシージャーにWM_DROPFILESが送られるので、このメッセージをフックしてドロップされたファイルを処理してやります。

LRESULT CALLBACK WindowProc(
  HWND hwnd,
  UINT uMsg,
  WPARAM wParam,
  LPARAM lParam
  )
{
  HDROP hdrop;
  char filename[MAX_PATH];
  int num, i;

  switch (uMsg) {
  case WM_DROPFILES:
    hdrop = (HDROP)wParam;
    /* ドロップされたファイルの個数を得る */
    num = DragQueryFile(hdrop, -1, NULL, 0);
    /* ファイルを処理 */
    for (i = 0; i < num; i++) {
      DragQueryFile(hdrop, i, filename, &filename);
      /*
       * filename にファイル名が入っているので、ここで処理を行う。
       */
    }
    DragFinish(hdrop);
    break;

  /* other messages... */
  }
  return 0;
}

これでExplorerからのファイルドロップを実装できました。しかし、まだファイルD&Dのうち後者しか実現していません。
次はExplorerへのファイルドラッグの実装に挑戦してみます。

OLE

“ドラッグ”は“ドロップ”と対になっている動作ですが、Explorerへのファイル“ドラッグ”に関しては残念ながら上記のように単純には実装できません。これを行うにはOLE(Object Linking and Embedding)を利用する必要があります。OLEはCOM(Component Object Model)技術に基づいているため、実際はCOMサーバーを実装することが必要になってきます。
ここで問題になるのは、COMがオブジェクト指向であるためC言語で実装するのが面倒だということです。C++ならばclassを利用して比較的楽に実装できるのですが、Cにはそのような言語的サポートはありません。よって少々煩雑なコーディングが必要になってきます。
以下の解説ではCOMに関する説明は省略します。COMについて上手に説明しているサイトは沢山ありますのから、そちらを参考にして下さい。

OLE Drag&Drop

さて、OLE D&Dを実装するにあたって次の3つのCOMインターフェイスを使う必要があります。

  • IDataObject
  • IDropSource
  • IDropTarget

IDataObjectは受け渡しするデータ(ここではファイル)であり、IDropSourceはOLE D&Dのドラッグ開始元に、またIDropTargetはOLE D&Dのドロップ先に対応しています。そして、OLE D&Dのドラッグ開始するにはDoDragDrop()関数を用います。
OLE D&Dにおける処理の流れは次のようになっています。


  1. DoDragDrop()関数を呼び出す。
  2. ドラッグ中のオブジェクトがウインドウ領域の上に入ると、IDropTarget::DragEnter()が呼び出される。
  3. IDropSource::GiveFeedback()が呼び出される。
  4. マウスがウインドウ領域の上にいる間は次の関数が呼び出された後、IDropSource::GiveFeedback()が呼び出される。

    • マウスカーソルが領域内で動くと、IDropTarget::DragOver()が呼び出される。
    • マウスカーソルがウインドウ領域外に移動すると、IDropTarget::DragLeave()が呼び出される。

  5. ドラッグ中にキー入力やマウスクリックが発生するとIDropSource::QueryContinueDrag()が呼び出される。ここで、D&Dを完了させる(ドロップ)かキャンセルするかを決定する。
  6. オブジェクトがドロップされると、IDropTarget::Drop()が呼び出される。
  7. DoDragDrop()関数から戻る。このときIDropTarget側で設定されたDROPEFFECTが戻り値として得られる。

ここで、IDropSourceとIDropTargetは別々のアプリケーションに属している(ことが多い)に注意してください。
例えばExplorerからのファイルD&Dの場合、IDropSourceはExplorer側で提供され、IDropTargetは自作アプリケーション側で提供したものが使われます。

IDropSource

まず、ファイルの“ドラッグ”を実現するためのIDropSourceインターフェイスを実装します。IDropSourceには次の2つのメソッドがあります。

  • IDropSource::GiveFeedback()
  • IDropSource::QueryContinueDrag()

GiveFeedback()メソッドではドラッグ先(IDropTarget)で指定されたD&Dの状態(DROPEFFECT)によって、SetCursor()関数等でマウスカーソルにフィードバックを与えます。このときDRAGDROP_S_USEDEFAULTCURSORSを返すことで、OLEがデフォルトで提供するカーソル形状を利用できます。
QueryContinueDrag()メソッドではキー入力やマウス入力によって、D&Dを完了するかキャンセルするかを返します。Windowsの一般的なGUIとしては、ESCが押されたらキャンセルしマウスボタンが離されたらD&D完了(オブジェクトのドロップ)とします。

typedef struct tagOLEDropSource {
  IDropSource super;
  ULONG       m_cRefCnt;
} OLEDropSource;

/* IUnknown methods */
STDMETHODIMP OLEDropSource_QueryInterface(/*略*/);
STDMETHODIMP_(ULONG) OLEDropSource_AddRef(/*略*/);
STDMETHODIMP_(ULONG) OLEDropSource_Release(/*略*/);

/* IDropSource methods */
STDMETHODIMP OLEDropSource_QueryContinueDrag(
  OLEDropSource *this,
  BOOL fEscapePressed,
  DWORD grfKeyState
  )
{
  /* ESCキーが押されたらD&Dキャンセル */
  if (fEscapePressed)
    return DRAGDROP_S_CANCEL;
  /* マウスボタンが離されたらD&D完了 */
  if (!(grfKeyState & (MK_LBUTTON|MK_RBUTTON)))
    return DRAGDROP_S_DROP;
  return S_OK;
}

STDMETHODIMP OLEDropSource_QueryContinueDrag(
  OLEDropSource *this,
  DWORD dwEffect
  )
{
  /* OLEデフォルトのカーソルを使用する */
  return DRAGDROP_S_USEDEFAULTCURSORS;
}
IDataObject

次にOLE D&Dで受け渡されるデータを表現するためのIDataObjectインターフェイスを実装します。IDataObjectには次の9つのメソッドがあります。

  • IDataObject::GetData()
  • IDataObject::GetDataHere()
  • IDataObject::QueryGetData()
  • IDataObject::GetCanonicalFormatEtc()
  • IDataObject::SetData()
  • IDataObject::EnumFormatEtc()
  • IDataObject::DAdvise()
  • IDataObject::DUnadvise()
  • IDataObject::EnumDAdvise()

これら全てのメソッドを実装するのは大変な作業なのため、ここではファイルを表すHDROPを格納するための最低限の実装だけ行うことにします。
実装するのは次の4つのメソッドと、EnumFormatEtc()メソッドで必要になるIEnumFORMATETCインターフェイスです。

  • IDataObject::GetData()
  • IDataObject::QueryGetData()
  • IDataObject::SetData()
  • IDataObject::EnumFormatEtc()

ここではIDataObject/IEnumFORMATETCの実装の表記は省略します。

OLE D&D: Drag編

ではExplorerへのファイルドラッグのための前準備からD&D開始、後処理までの流れをコーディングします。

/*
 * ファイル名を'\0'で区切った文字列。末尾は2個の'\0'で終わる。
 */
char *files = "c:\file1.txt\0c:file2.txt\0\0";

IDataObject *lpDataObject = /* IDataObjectのインスタンス */;
IDropSource *lpDropSource = /* IDropSourceのインスタンス */;
FORMATETC fmtetc;
STGMEDIUM medium;
char *p;
DWORD effect;
HRESULT hr;


/* FORMATETC構造体をセット */
fmtetc.cfFormat = CF_HDROP;
fmtetc.ptd = NULL;
fmtetc.dwAspect = DVASPECT_CONTENT;
fmtetc.lindex = -1;
fmtetc.tymed = TYMED_HGLOBAL;

/* STGMEDIUM構造体をセット */
medium.tymed = TYMED_HGLOBAL;
medium.hGlobal = GlobalAlloc(
                   GMEM_MOVEABLE, 
                   sizeof(DROPFILES) + sizeof(files)
                 );
medium.pUnkForRelease = NULL;
p = GlobalLock(medium.hGlobal);
((DROPFILES *)p)->pFiles = sizeof(DROPFILES);
((DROPFILES *)p)->fWide = FALSE;
CopyMemory(p + sizeof(DROPFILES), files, sizeof(files));
GlobalUnlock(medium.hGlobal);

/* IDataObjectをセット */
IDataObject_SetData(lpDataObejct, &fmtetc, &medium, FALSE);

/* OLE Drag & Drop開始 */
hr = DoDragDrop(
       lpDataObejct,
       lpDropSource,
       DROPEFFECT_MOVE|DROPEFFECT_COPY|DROPEFFECT_LINK,
       &effect
     );

/* リソースを開放 */
IDataObject_Release(lpDataObejct);
IDropSource_Release(lpDropSource);

/*
 * hr, effectの値によってデータを処理...
 */

実際のコードはマウスのドラッグ開始メッセージなどによって実行されるようにします。これはWM_LBUTTONDOWN, WM_RBUTTONDOWN, LVN_BEGINDRAG(リストビュー), TVN_BEGINDRAG(ツリービュー)等が相当します。
これでExplorerへのファイルD&Dが実装できました。最初の非OLE D&DのExplorerからのファイルドロップ受付と、この実装を組み合わせればExplorerとの完全なD&Dが実現できます。
しかし、せっかくOLE D&Dの“ドラッグ”機能を実装したので、“ドロップ”機能もOLEを利用して実装することにします。

IDropTarget

では、ファイルの“ドロップ”を実現するためのIDropTargetインターフェイスを実装します。IDropTargetには次の4つのメソッドがあります。

  • IDropTarget::DragEnter()
  • IDropTarget::DragOver()
  • IDropTarget::DragLeave()
  • IDropTarget::Drop()

DragEnter()/DragOver()/DragLeave()メソッドは対象ウインドウ領域内にマウスカーソルが"入った"/"移動した"/"出た"という動きに対応して呼び出されます。これらのメソッド内でキー入力に応じてDROPEFFECTを指定することにより、IDropSourceが視覚的フィードバックを行なうめの情報を渡します。
Drop()メソッドは実際にオブジェクトが対象ウインドウにドロップされると呼び出されます。ここでIDataObjectを介して実際にデータのやり取りを行ないます。
今回はOLE D&Dのターゲットウインドウ側の(視覚的な)フィードバックは考慮していませんが、何らかのフィードバックを行いたい場合は各メソッドでリソースの初期化/破棄を行なう必要があります。

typedef struct tagOLEDropTarget {
  IDropTarget super;
  ULONG       m_cRefCnt;
} OLEDropTarget;


/* IUnknown methods */
STDMETHODIMP OLEDropTarget_QueryInterface(/*略*/);
STDMETHODIMP_(ULONG) OLEDropTarget_AddRef(/*略*/);
STDMETHODIMP_(ULONG) OLEDropTarget_Release(/*略*/);

/* IDropTarget methods */
STDMETHODIMP OLEDropTarget_DragEnter(
  OLEDropTarget *this,
  IDataObject *pDataObject,
  DWORD grfKeyState,
  POINTL pt,
  DWORD *pdwEffect
  )
{
  /*
   * pDataObject に応じてドロップを受け付けるかを判断
   * ドロップを拒否する際は
   *   pdwEffect = DROPEFFECT_NONE;
   * とする。
   */
  
  /*
   * キー押下の状態に応じて pdwEffect をセット
   */
  return S_OK;
}

STDMETHODIMP OLEDropTarget_DragOver(
  OLEDropTarget *this,
  DWORD grfKeyState,
  POINTL pt,
  DWORD *pdwEffect
  )
{
  /*
   * キー押下の状態に応じて pdwEffect をセット
   */
  return S_OK;
}

STDMETHODIMP OLEDropTarget_DragLeave(
  OLEDropTarget *this
  )
{
  return S_OK;
}

STDMETHODIMP OLEDropTarget_Drop(
  OLEDropTarget *this,
  IDataObject *pDataObject,
  DWORD grfKeyState,
  POINTL pt,D
  WORD *pdwEffect
  )
{
  /*
   * D&D完了時の処理...
   */
  return S_OK;
}

OLE D&D: Drop編

IDropTargetの準備ができたら、Explorerからのファイルドロップを受け付けるようコーディングしてみます。

RegisterDragDrop()関数を用いて、IDropTargetインターフェイスとファイルD&Dを受け付けるウインドウを関連付けます。

HWND hwnd = /* 対象ウインドウのハンドル */;
IDropTarget *lpDropTarget = /* IDropTargetのインスタンス */;

RegisterDragDrop(hwnd, lpDropTarget);

これでOLE D&Dの受け入れが登録され、ファイルがドロップされると関連付けたIDropTargetのDrop()メソッドが呼び出されるようになりました。
逆に登録を解除するにはRevokeDragDrop()関数を用いて次のようにします。

HWND hwnd = /* 対象ウインドウのハンドル */;

RevokeDragDrop(hwnd);

なおIDropTargetの参照は登録時にAddRef()/解除時にRelease()されるので明示的にRelease()する必要はありません。

OLE D&D: まとめ

これらのOLE D&Dを利用するためには、OLEライブラリの初期化を先立って行なわなければいけません。これはOleInitialize()関数で行ないます。また、利用しなくなったらOleUninitialize()関数で解放する必要があります。
これらはWinMain()で他の初期化/解放処理と一緒に行なったほうが良いでしょう。

int WINAPI WinMain(
  HINSTANCE hInstance,
  HINSTANCE hPrevInstance,
  LPSTR lpCmdLine,
  int nCmdShow
  )
{
  /* OLEライブラリを初期化 */
  OleInitialize(NULL);
  
  /*
   * メイン処理...
   */
  
  /* ライブラリを解放 */
  OleUninitialize();
  return 0;
}

あとがき

これで当初の目的であるExplorerとのファイルD&Dは実装できました。ただしIDataObjectの実装は手抜きですからIAdviseSinkを利用できません。これはソース側にオブジェクト(IDataObject)の変更を通知するためのインターフェイスです。また、Windows2000からはIDropSourceHelper/IDropTargetHelperというインターフェイスが追加されて、視覚的フィードバックの実装の手助けをしてくれるようです。
今回利用したOLE D&DではファイルのD&D以外にも様々なデータ(オブジェクト)のD&Dに対応しています。例えばMicrosoft Word等では選択したテキストをD&Dによってコピー&ペーストすることができますが、このようなテキストもOLE D&Dで受け取ることができます。
これらについての情報は、IDataObjectやFORMATETCといったキーワードから検索できると思います。

C++スレッド遅延開始の実装5パターン

局所的に「スレッド開始を遅延させる」ネタが盛り上がっていたので、C++とBoostライブラリを用いた色々な実装方法をまとめてみました。

この記事で対象とするのは、下記コードにある2つの要件を満たす実装方法です。

  • (1) スレッドを管理するオブジェクトXのコンストラクト時ではなく、その後の任意タイミングで新スレッド処理を開始する。
  • (2) オブジェクトXのデストラクト時に、上記(1)の別スレッドがまだ実行中ならそのスレッド処理完了を待機する。
class X {
  // threadオブジェクトを保持するメンバ変数

  void do_() { /* 別スレッド処理 */ }

public:
  ~X()
  {
    // (2) 別スレッドがまだ実行中なら完了を待機する
  }

  void start()
  {
    // (1) 新しいスレッドを開始して関数do_を実行する
  }
};

int main()
{
  X x;
  x.start();
}

実装方法の比較

#1,2,3は@cpp_akiraさんの std::threadをあとから開始。それとムーブ対応したコンテナについて - Faith and Brave - C++で遊ぼう、#4は@egtraさんのイグトランスの頭の中(のかけら) » Boost.ThreadとBoost.Optionalからのパク(ry引用です。#5が新たに追加した実装方法です。

実装方法 C++03/Boost C++11/Boost C++11/std
#1 SmartPtr+thread
#2 thread.swap
#3 threadムーブ
#4 Boost.Optimal+thread ×
#5 future+async() × ×

表中の"C++03/Boost"はC++03準拠コンパイラ+Boostライブラリ、"C++11/Boost"はC++11準拠コンパイラ+Boostライブラリ、"C++11/std"はC++11準拠コンパイラ+標準ライブラリのみの範囲において、各実装方法が実現可能/不可を示します。(なお、Boost 1.49.0を前提としています。)

注意:後述コードサンプルではC++11ラムダ式を使用しており、C++11非準拠コンパイラではコンパイルできません。C++03の範囲内では下記の書き換えが必要です。

boost::thread([this] { do_(); })
// ↓
boost::thread(&X::do_, this)

#1: SmartPtr+thread

#include <boost/shared_ptr.hpp>
#include <boost/thread.hpp>

class X {
  boost::shared_ptr<boost::thread> thread_;

  void do_() { /*...*/ }

public:
  ~X()
  {
    if (thread_)
      thread_->join();
  }

  void start()
  {
    thread_.reset(new boost::thread([this] { do_(); }));
  }
};

(個人的には、コピー可能なスマートポインタshared_ptrより、ムーブのみを許容するunique_ptrの方が好ましい気がします。)

#2: thread.swap

#include <boost/thread.hpp>

class X {
  boost::thread thread_;

  void do_() { /*...*/ }

public:
  ~X()
  {
    if (thread_.joinable())
      thread_.join();
  }

  void start()
  {
    boost::thread thd([this] { do_(); });
    thread_.swap(thd);
  }
};

#3: threadムーブ

#include <thread>

class X {
  std::thread thread_;

  void do_() { /*...*/ }

public:
  ~X()
  {
    if (thread_.joinable())
      thread_.join();
  }

  void start()
  {
    thread_ = std::thread([this] { do_(); });
  }
};

#4: Boost.Optimal+thread

#include <boost/optional.hpp>
#include <boost/utility/in_place_factory.hpp>
#include <boost/thread.hpp>

class X {
  boost::optional<boost::thread> thread_;

  void do_() { /*...*/ }

public:
  ~X()
  {
    if (thread_)
      thread_->join();
  }

  void start()
  {
    thread_ = boost::in_place([this] { do_(); });
  }
};

#5: future+async()

この実装方法は前述のものとは異なり、threadオブジェクトの明示的な生成/保持を行いません。代わりに、launch::asyncポリシーasync関数によるスレッド開始と、futureオブジェクトのデストラクタによる(暗黙的)スレッド完了待機を行います。暗黙的なスレッドjoinについてはasync関数launch::asyncポリシーとfutureのちょっと特殊な動作 - yohhoyの日記を参照下さい。

#include <future>

class X {
  std::future<void> task_;

  void do_() { /*...*/ }

public:
  // task_がlaunch::async起動されたfutureオブジェクトの場合に限り、
  // future<void>::~future()にてスレッドjoinが暗黙的に行われる。

  void start()
  {
    task_ = std::async(std::launch::async, [this] { do_(); });
  }
};

実装#1~#4では関数do_()から例外送出されるとプログラム終了(std::terminate)しますが、実装#5は関数do_()で送出した例外を無かったこととして処理続行します。正確に表現すると、例外はちゃんとfutureオブジェクト中に格納されています。上記コードではfuture::get()による処理結果の取り出しを行っておらず、結果として関数do_()が送出した例外が失われています。

各実装はgcc 4.6.3@Ubuntu 11.10にて動作確認済みです。(なお#5はgcc 4.7.0@MacOS X 10.6だとdo_からの例外送出時のみSegmentation Faultが発生。)