yohhoyの日記(別館)

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

volatile教、あるいはvolatile狂

かつてのMicrosoft Visual Studio .NET 2003のC/C++コンパイラ(MSVC7.1)には、「volatile変数にオレオレ定義の意味を与えて最適化を行う」というアグレッシブすぎるオプションが存在したという昔話。

どんなもの?

このオプションでは、volatileキーワードにC/C++標準規格とは全く異なる独自の意味を与えて、コード生成時の最適化処理に利用します。つまり、コンパイル時に下記を前提としたコード生成を行うのです。

  • 通常の変数=変数実体にアクセスする手段は1通りだけ(aliasingが無い)
  • volatile変数=上記制約の範囲外(aliasingが有る)

これは「volatile=aliasingが有りえる」というMSVC7.1だけの独自拡張です。*1

このaliasing(エイリアシング; 別名)という単語、少々耳慣れないかもしれません。aliasingが無いとは、“ある変数が指す実体にアクセス”するとき、その実体への“別の変数経由でのアクセス”が存在しないことを意味します。具体例を挙げると、int a = 0;に対してint *p = &a;を定義したとき、a*pは“同一の実体(メモリ)”を指します。その後の処理でa = 42;int b = *p;のように異なる変数を介してアクセスするとき、この実体に対してはaliasingが有ると言えます。*2

実はこのaliasingの有無は、コンパイラの最適化処理にとって非常に厄介なものです。通常はaliasingが有ると仮定を置いたコード生成を行わざるをえず、積極的な最適化ができないというケースが多々あります。*3

具体的には何をするの?

この最適化オプションの働きについて、マニュアル(MSDN)を引いてみましょう。

/Oa または /Ow を使用する場合は、次の規則に従う必要があります。次の規則は、volatileとして宣言されていないすべての変数に適用されます。

  • 直接使われている変数をポインタで参照しません。変数が代入式の左辺または右辺で使われているか、あるいは関数の引数として使われていると、その変数が参照されます。
  • 変数へのポインタが使われている場合は、その変数を直接使用できません。
  • 変数のアドレスを関数で取る場合は、その変数を直接使用できません。
  • ポインタによってメモリ位置が変更された場合は、別のポインタでそのメモリ位置にアクセスできません。

上記の規則に従わないと、データが破損する可能性があります。

/Oa、/Ow (エイリアスを使わないと仮定、関数呼び出しでエイリアスを使うと仮定)

…上記のMSDN日本語訳では意味が取りづらい気がしますので、素直に英語版MSDNを参照すべきかもしれません。

If you use /Oa or /Ow, you must follow these rules. The following rules apply for any variable not declared as volatile:

  • No pointer can reference a variable that is used directly (a variable is referenced if it is on either side of an assignment or if a function uses it in an argument).
  • No variable can be used directly if a pointer to the variable is being used.
  • No variable can be used directly if its address is taken within a function.
  • No pointer can be used to access a memory location if another pointer modifies the same memory location.

Failing to follow these rules can cause corrupted data.

/Oa, /Ow (Assume No Aliasing, Assume Aliasing Across Function Calls)

ちょっと何言ってるか分からない

MSDNに掲げられた4つのルールに従うと、下記ソースコードを/Oaオプションありでコンパイルした場合、実行時にデータが壊れて悲惨な結果が得られるのことです(おそらく、予期しない値を読み込んだように見えるでしょう)。いずれもC/C++言語ではごくありふれた処理ですし、このようなソースコードが正しく実行できないとなると、もはや普通のプログラムを書ける気がしません。

int x = 0;
int *p;
void rule1()
{
  x = 42;  // 直接xを使うとき...
  p = &x;  // NG: ポインタが参照してはダメ!
}
int x = 0, y;
int *p = &x;
void rule2()
{
  *p = 42;  // ポインタ経由で使うとき...
  y = x;    // NG: 直接xを使ってはダメ!
}
int x = 0;
void rule3()
{
  int *p = &x;  // 関数内でアドレスをとると...
  x = 42;       // NG: 直接xを使ってはダメ!
}
int x = 0, y;
int *p1 = &x;
int *p2 = &x;
void rule4()
{
  *p1 = 42;  // あるポインタ経由で使うと...
  y = *p2;   // NG: 別ポインタ経由で使ってはダメ!
}

volatile教のすゝめ

MSVC7.1曰く、/Oaオプションありでも正常動作させるならば、volatileキーワードを利用せよと。ここまで来ると、もはやvolatile教信者というよりvolatile狂信者の様相を呈しています。

volatile int x = 0;
volatile int *p;
void rule1()
{
  x = 42;  // 直接xを使うとき...
  p = &x;  // ポインタが参照してもOK!
}
volatile int x = 0, y;
volatile int *p = &x;
void rule2()
{
  *p = 42;  // ポインタ経由で使うとき...
  y = x;    // 直接xを使ってもOK!
}
volatile int x = 0;
void rule3()
{
  volatile int *p = &x;  // 関数内でアドレスをとると...
  x = 42;       // 直接xを使ってもOK!
}
volatile int x = 0, y;
volatile int *p1 = &x;
volatile int *p2 = &x;
void rule4()
{
  *p1 = 42;  // あるポインタ経由で使うと...
  y = *p2;   // 別ポインタ経由で使ってもOK!
}

Don't Try This At Home, I'm A Professional Volatileian.

その後…

結局のところ、実プロダクトでこの最適化オプションを活用できる場面が無かったのでしょう。次バージョンMicrosoft Visual Studio .NET 2005(MSVC8.0)ではあっさり削除されましたとさ。

  • /Oaコンパイラオプションが削除されましたが、エラーなしで無視されます。(後略)
  • /Owコンパイラオプションが削除されましたが、エラーなしで無視されます。(後略)
Visual Studio 2005 - コンパイラの新機能

R.I.P to /Oa, /Ow options.

*1:C/C++標準規格では(主として)「volatile=C/C++実行環境の“外”にリンクしている」を意味し、代表的な例ではメモリマップドI/Oなどでvolatile変数が利用されます。よく「volatile=最適化の抑止」のような表現がされますが、これは「volatile変数アクセス⇒C/C++言語仕様の“外”への干渉/未知の“何か”を操作する⇒コンパイラが直接知りえない操作は勝手に最適化できない」から帰結される一側面に過ぎません。

*2:本記事中では言及しませんが、C言語(C99以降)にはrestrictキーワードが導入されており、この最適化オプションが目指していた事をより安全に達成できます。つまり、プログラマ自身が「aliasingが無い」と分かっている箇所のみを明示し、普通の変数アクセスでは従来通り「aliasingが有る」という仮定が置かれます。詳細はrestrictキーワード続 restrictキーワードなどを参照ください。

*3:C/C++言語の文脈では“Strict Aliasing Rule”という規則があり、これにより一定の条件下ではaliasingが無いと仮定した積極的な最適化処理ができます。詳細は翻訳記事“C/C++のStrict Aliasingを理解する”あたりをどうぞ。

FizzBuzz化ストリーム

インターネット上で定期的に話題にのぼる「FizzBuzz問題」を、C++言語で変な風に解いてみたというお話。

今ではすっかり有名になったFizzBuzz問題ですが、元々はJeff Atwood氏による2007年の記事Why Can't Programmers.. Program?青木靖氏による日本語訳)が初出のようです。ルールをおさらいしておくと、次の仕様を満たすプログラムを作れという問題ですね。

1から100までの数をプリントするプログラムを書け。ただし3の倍数のときは数の代わりに「Fizz」と、5の倍数のときは「Buzz」とプリントし、3と5両方の倍数の場合には「FizzBuzz」とプリントすること。

困ったときのおまじない

FizzBuzz問題はあまりにも有名なため、Web検索すればいくらでも実装コード例が見つかります。いまさら普通に解いてもしょうがない(?)ので、C++標準ライブラリI/Oストリームを利用して非侵襲的(nonintrusive)な解法をとりたいと思います。

つまり、オリジナルのこんなコードに対して:

#include <iostream>
const int N = 16;

// 1...Nまでの数字を出力
template <class OutputStream>
void countup(OutputStream& os)
{
  for (int i = 1; i <= N; ++i)
    os << i << os.widen(' ');
  os << std::endl;
}
 
int main()
{
  // countup()関数を呼ぶ
  countup(std::cout);
}
// 出力結果:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

標準出力ストリームstd::coutに小細工をすると…

int main()
{
  fizzbuzznizer fb(std::cout);  // おまじない

  // 先ほどと同じcountup()関数を呼ぶだけ
  countup(std::cout);
}
// 出力結果:
// 1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz 16

元の出力処理(countup関数)には一切手を加えずに(=非侵襲的)、ちゃんとFizzBuzz問題が解けました。

ちなみにこの“おまじない”、出力処理側のコードには依存しないので:

fizzbuzznizer fb(std::cout);  // おまじない

// 出力が文字列でもOK
std::cout << "seq={1 2 3 4 5 6...}";
  // 出力結果:seq={1 2 Fizz 4 Buzz Fizz...}

// Boost.Formatとの組み合わせもOK
#include <boost/format.hpp>
std::cout << boost::format("%1% and %2%, %3%") % 1 % 42 % 105;
  // 出力結果:1 and Fizz, FizzBuzz

また出力ストリーム(std::ostream派生クラス)を対象として、こんなことも:

#include <sstream>
std::ostringstream oss;
fizzbuzznizer fb(oss);  // おまじないをossにふりかける

// ostringstreamでもいけるよ
oss << 6 << "/10";
assert(oss.str() == "Fizz/Buzz");

C++万歳!I/Oストリーム万歳!

タネ明かし

先ほど使った怪しい“おまじない”は、下記コードで実現されます。
実装の概略:

  • basic_fizzbuzznizer:対象出力ストリームオブジェクトのストリームバッファを、下記basic_fizzbuzz_streambufに差し替える。
  • basic_fizzbuzz_streambuf:対象ストリームオブジェクトから渡されてくる文字に対してFizzBuzz化を行い、結果を元ストリームバッファに転送する。
  • ジェネリックな記述によりwchar_t等にも(無駄に)対応。


basic_fizzbuzznizerクラステンプレート

同オブジェクトの変数スコープにあわせて、対象出力ストリームオブジェクト(std::coutなど)のストリームバッファを置換/復元するヘルパクラスです。

通常は「std::cout → {coutのストリームバッファ}」のようになっている関連付けを、コンストラクタにて「std::cout → basic_fizzbuzz_streambuf → {coutのストリームバッファ}」のように差し替え、またデストラクタでは関連付けを元に戻しています。

basic_fizzbuzz_streambufクラステンプレート

実際にFizzBuzz化処理を行う、std::basic_streambufから派生するストリームバッファクラスです。basic_fizzbuzz_streambufは一般的なストリームバッファと異なり、ストリームクラスから渡されてくる文字を変換(FizzBuzz化)するだけで、本当の“出力”処理は元のストリームバッファへ委譲します。

さて、FizzBuzz問題における倍数判定は「数値」に対して行う必要がありますが、残念ながらストリームバッファで受け取れるデータは「文字」シーケンスだけです。これは前段のストリームオブジェクトで整形(format)された結果であり、I/Oストリームライブラリ設計に起因しています。

ただ幸いなことに必要な倍数判定は3と5のため、次の代替判定アルゴリズムを利用できます。また、与えられた「文字」シーケンスに対して、“数字で構成される部分シーケンス”を「数値」として解釈することにします。

  • 3の倍数=各桁の数値の合計値が3の倍数
  • 5の倍数=最下位桁が0または5

以上をもとに、basic_fizzbuzz_streambuf各メンバ関数では次の処理を行います。

コンストラクタ
元ストリームバッファを保持し(sb_)、自身の出力領域(put area)サイズを0に設定する(setp(0, 0))。これにより、1文字受け取るたびにoverflow()が呼び出されるようになる。
overflowオーバーライド
出力領域があふれる=ストリームバッファへ1文字入力された時に呼び出される。入力が“数字”であれば一時バッファ(tb_)で追加保持し、3の倍数判定準備(acc3_)と5の倍数判定(mul5_)を行う。数字以外の場合はそのまま元ストリームバッファへ転送する(sb_.sputc(c))。
syncオーバーライド
フラッシュ要求がなされた時に呼び出される。倍数判定結果に応じて出力文字列を決定し、putn()ヘルパ関数を経由して元ストリームバッファへ流し込む(sb_.sputc(...))。
fizzbuzznizer

仕上げに、C++標準ライブラリ風のchar/wchar_t用typedefを準備して完成です。

typedef basic_fizzbuzznizer<char> fizzbuzznizer;
typedef basic_fizzbuzznizer<wchar_t> wfizzbuzznizer;

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

この記事は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;
}