2010-09-30

プログラマーが読んで楽しめる本

全プログラマーが読んで楽しめる本とは何か。最良の本ということになれば、これは、そのプログラマーの信仰する言語によって、異なるであろう。

例えば、C言語厨はK&Rとか言う古書を聖書と定めている。LISP信者は全員、SICPという魔法の経文を所有しており、毎日欠かさずに読経するそうである。また、各人が確実にSICPを読んだことを確認しあう意図なのであろう。LISP信者同士の挨拶は、"Have you read your SICP today?"である。C++信者にとっては、ISOの規格書が最良の本だろう。コンパイラ屋は、銘の刻まれた剣と盾と鎧を装備した騎士が、ドラゴンと向かうあう本を、座右に置いているらしい。これにはどういう言われがあるのか分からないが、たぶんゲームの攻略本か何かであろう。

話がそれた。ここでは、プログラマーの宗教、もとい言語や専門分野に関わらず、一般に楽しめる本を紹介する。

G Pascal Zachary著:Showstopper! the Breakneck Race to Create Windows Nt and the Next Generation at Microsoft
邦訳:闘うプログラマー ビル・ゲイツの野望を担った男達

Windows NTの開発秘話。NTカーネルの設計者であるDave Cutler様を主人公にして話が展開される。

Clifford Stoll著The Cuckoo's Egg: Tracking a Spy Through the Maze of Computer Espionage
邦訳:カッコウはコンピュータに卵を産む〈上〉カッコウはコンピュータに卵を産む〈下〉

伝説のハッカーの一人、Clifford Stallの自著による、不正アクセスの発見と監視の記録。

長谷川 裕行著:ソフトウェアの20世紀―ヒトとコンピュータの対話の歴史

コンピューター史をひと通り、画像付きで解説している歴史書。

とりあえず、自分の読んだ中で、特に面白いと思ったものを挙げた。他にも、90年代前半までのコンピューターウイルスの歴史を解説した良書があったはずだが、本の題名を忘れた。

他にも、マイ・コンピュータをつくる―組み立てのテクニックという、ブルーバックスシリーズの本があるのだが、これは少し、人を選ぶかもしれない。内容は、8086を使った自作コンピューターの作り方を説明する本である。とはいっても、今となってはそれほど役には立たないだろうし、電子工作が好きな人向けか、当時の組み立て済みではない自作マイコンの歴史的資料を探している人向けだろうか。

10代で読んでも理解出来ない書物

10代で読んでいないと恥ずかしい必読書 - その1 - PictorialConnect

思うに、これらの書物は、哲学である。数学や物理学や機械工学などではない。してみれば、これらの書物は、原文で読まなければ、真に「読んだ」とは言えぬのではなかろうか。なぜならば、数学などの学問は、どの言語で定義しているかということは、問題にならないはずである。しかし、思想は、言語の影響を受ける。しかも、これらは口頭で語られたものではなく、文章である。中には、スピーチを文章に落とし込んだものもあるが、結局、文章であり、聞くのでなく、読むことを前提にしていることには変りない。

しかし、このリンク先の人物は、古ギリシャ語やラテン語に加えて、当時の(現代語とは多少異なる)ドイツ語、フランス語、デンマーク語、英語、イタリア語を、すべて読むことができるのであろうか。ザメンホフや柳沼重剛のような言語オタクならともかく、果たして本当にそのような言語オタクなのだろうか。もしそうでなく、どれか一冊でも翻訳で読んでいるのであれば、彼は自分の発言を恥づべきである。

むしろ逆に、10代では読んでも理解出来ない書物を挙げたほうが役に立つのではないか。

たとえば、芥川龍之介だ。彼の文章を真に理解するためには、今昔物語集を読破しなければならない。私も、今昔物語集を、最近になって読み終えて、始めて彼の文章の真の素晴らしさが分かった。しかし、現代人は、10代のうちに今昔物語集を読破するのは難しい。

高山樗牛もすばらしい小説をひとつだけ書いたが、あの文章が以下にすばらしいかを理解するためには、源平盛衰記を読破しなければならない。これも、10代では、難しいだろう。

古文や漢文も、10代の頃から読んではいたが、当時は、本当の面白さを理解してないなかった。もっとも、私のことだから、30代になれば、「20代の頃は、古文漢文の本当の本当の面白さを理解していなかった」などと思うのであろう。

これを思うに、結局、時間が足りないということだろうか。

では逆に、10代のうちから楽しめる書物は何か。私の経験では、太宰治、中島敦、吉川英治である。彼らの書く文章は独特の勢いがあり、特に難しいことを考えなくても読める。ただし、20代になると、もう楽しむことはできない。というのも、彼らの小説は、筋書きが違うだけで、後はすべて同じだからだ。太宰治は、陰鬱な文章ばかり書いているし、中島敦は、人より優れていながら、何らかの理由によって成り上がれないでいる主人公の話ばかりだ。これは、作者の性格と環境が影響しているのであろう。吉川英治は、王道とも言うべき大筋を、どの小説に対しても適用する。特に不自由せず暮らしていた人物が、ある問題を発端として、これまでの人生を否定せねばならず、その結果として放浪の旅があり、最後には問題を円満に解決するといったものだ。

2010-09-28

NINTENDO64は64だった!


Digg - Nintendo 64 coincidence? (PIC)

あのNINTENDO64のロゴは、頂点数もポリゴン数も、ちょうど64個で構成されていたらしい。実際に自分でダンプして確かめたわけではないので、真偽は分からないが、任天堂ならば、さもありなんと思える遊び心だ。

2010-09-26

国勢調査

風邪を引いたらしく、朝から寝ていた。三時頃に、チャイムの音で起こされた。出てみると、国勢調査らしい。用紙を渡された。

前に、日本のプログラマーの人口を推定するという話で、国勢調査があるということを知った。その時の調べで、今年の10月だと知っていたので、そろそろだとは思っていたが、すっかり忘れていた。ともかく、これで、2010年のプログラマー人口を推定できるかもしれない。楽しみだ。

しかし、私の仕事はプログラマーと言えるかは疑問だ。早く参考書を書き終えなければ。

起きて数十分ぐらいは、だいぶ風邪も治ったかと思ったが、一時間経つと、明らかに体調が悪くなってきた。残念ながら、風邪が治るには、まだもうしばらくかかりそうだ。それにしても、こう季節の変わり目ごとに、風邪をひいていては、キリがない。毎年毎年、夏から冬にかけてと、冬から春にかけてと、気候が急激に変わるときに、体調を崩しているのだ。親は決まって、不養生と生活環境とを挙げるが、どうもこれは、体質なのではないかと思う。

しかし、今回の風邪は、比較的マシな方だと思う。鼻水と喉が痛いだけで、頭痛や、気だるさなどは、常よりは少ない。少なくとも、横になっていれば、ほとんど頭痛や体のだるさはない。

ちなみに、今昔物語集では、(かぜ)という表記で、ちらほら出てくる。

2010-09-25

std::initializer_listはどのように実装されるのか

初期化リストを使うにあたって、初心者が信じてしまうかもしれないと懸念される問題は、std::initializer_listは、通常の配列に比べて、遅いのではないかという迷信だ。規格によれば、std::initializer_listは、通常の配列をautomatic storage、もしくはstatic storage上に構築し、その配列へのポインターを格納するのと、何ら変わることはない。

例えば、以下のようなコードは、

void f() 
{
    for ( auto i : { 1, 2, 3 } )
    {
        std::cout << i << std::endl ; 
    }
}

最終的に、以下のように置き換えることができる。

void f()
{
    int a[3] = { 1, 2, 3 } ;
    int * iter = a ;
    int * last = a + 3 ;
    for ( ; iter != last ; ++iter )
    {
        auto i = *iter ;
        std::cout << i << std::endl ;
    }
}

もちろん、細部をどのように実装するかについては、実装により異なる。しかし、初期化リストの各要素を格納するオブジェクトは、automatic storageかstatic storage上に構築可能である。

これを考えれば、std::minとstd::maxを、initializer_listで実装するか、Variadic Templatesで実装するかというのは、パフォーマンスを考えて見れば、ささいな違いではないかと思う。

したがって、std::initializer_listのパフォーマンスを心配するのは、「static変数やローカル変数、関数の実引数や戻り値のオブジェクトは、実装によってどこかよく分からないストレージ上に構築されるので信用ならん」と言うのに等しい。

いや、メモリーじゃなくてストレージという言葉が出てくる時点で、こう考えることはあり得ないか。

風邪

どうやら、風邪をひいたらしい。季節の変わり目になるといつもこうだ。

2010-09-23

ささやかな楽しみ

コンパイラの規格違反を発見するのが、私のささやかな楽しみとなっている。もちろん、VC10では、規格通りの解釈されるコードを探すほうが難しいので、まったくもってやりがいがない。gccには、なかなかそういうことがないので、規格違反を発見したときは、実にいい気分になる。たいてい、数ヶ月に一度ぐらいしか、規格違反は見つけられないのが困りものだが。

このたび、面白いバグを発見した。ひょっとしたら、まだどのコンパイラも正しく実装していないだけなのかもしれないが、バグには違いない。

メンバーテンプレート関数は、クラスの「コピー」のために、インスタンス化されることはない。

struct S
{
    S() = default ;
    template < typename T >
    S( T && ) { std::cout << "bad copy constructor" << std::endl ; }

    template < typename T >
    S & operator = ( T && ){ std::cout << "bad copy assignment operator" << std::endl ; return *this ; }
} ;

int main()
{
    S a ;
    S b = a ; // use trivial copy constructor
    b = a ; // use trivial copy assignment operator
}

このコードは、コピーのために、trivialなコピーコンストラクターとコピー代入演算子を使う。メンバーテンプレート関数はインスタンス化されない。したがって、このコードを実行しても、何も出力されない。

ところが、関数の仮引数がrvalueリファレンスの場合、VC10は、テンプレートなコンストラクターを呼び出してしまった。gccに至っては、コンストラクターに加えて、代入演算子も呼び出してしまっている。これは誤りである。

何故、コンパイラは、こんな単純な間違いを犯したのか。それは、インスタンス化されないのは、あくまで「コピー」に対しての話だからである。ムーブに対しては、問題なくインスタンス化される。

// 上記のクラス定義を使う

int main()
{
    S a ;
    S b = std::move(a) ; // use member template function
}

このコードは、テンプレート版の代入演算子をインスタンス化して、ムーブのために使う。その結果、bad copy assignment operatorと出力される。この挙動は正しい。

また、テンプレートパラメータへのrvalueリファレンスを、関数の仮引数にすると、argument deductionで、lvalueリファレンスにもなりうるという仕様がある。

template < typename T > void f( T && ) ;

int main()
{
    int object ;
    f( object ) ; // Tはint &
    f ( std::move( object ) ) ; // Tはint &&
}

このため、単にメンバーテンプレート関数であればインスタンス化を禁止するというだけではだめで、コピーかムーブかという違いを、コンパイラがしっかり認識していなければならない。コンパイラの実装には詳しくないが、まあ、ちょっと仕様が複雑だというのは理解できる。

ムーブが言語機能に取り入れられたのは、かなり最近の話だが、それだったら軒並みインスタンス化されなくてもいいものを、何でインスタンス化してしまうのか。まあ、軒並み禁止してしまったら、まともにrvalueリファレンスが使えなくなるので、ムーブを言語に取り入れていなかった規格の問題だったというべきなのかもしれない。

Google日本語入力の開発版が更新された

Google Japan Blog: Google 日本語入力の開発版をアップデートしました。(0.13.481.10x)

どうやら今回は、開発版ユーザーの中でも、一部だけにアップデートがかかるらしい。そしてどうやら、私はその一部の開発ユーザーになったらしく、アップデートされた。

Windows版のIMEのAPIが、TSFからIMMに退化(?)したのはどういう理由があるのだろう。

今回は、辞書の更新も入っているらしく、当て字が大幅に強化されたらしい。また、開発者は、Twitterでこんなことを言っている。

Twitter / Taku Kudo:

当て字が大幅にサポートされました。超電磁砲とか、約束された勝利の剣(えくすかりばー)なんてのも変換できます。信頼性の高い当て字をウェブからマイニングしています。

Twitter / Taku Kudo:

超電磁砲は、「れーるがん」で変換することができた。約束された勝利の剣には、どのような由来があるのやら。ググッたところ、有名な同人ゲーム、Fate/stay nightに由来するようだが。

追記:Fateは同人ゲームではないというツッコミを受けたので訂正。調べたところ、Fateの販売元であるTYPE-MOONは、昔は同人サークルであったが、いまは同人を卒業して、商業でやっているらしい。商業ベースで出した一作目が、Fateなのだとか。

参考:
Fate/stay night - Wikipedia
TYPE-MOON - Wikipedia

また、前回の開発版には、Windows版がなかったが、今回からは、辞書に顔文字も入っているようだ。「かおもじ」で変換したところ、548件の顔文字候補がでてきた。

せっかくなので、当て字の変換をさせて遊ぼうかと思ったが、ふと気がついてみれば、もうかれこれ五年以上、マンガもアニメもゲームもラノベも読んでいないのである。中二病全開の当て字はさっぱり分からない。

ともかく、あとは、古語と旧仮名遣いがあらまほしきかな。

あやうくアグリゲートにとんでもない嘘を書くところだった

すべての配列はアグリゲートである。ところが、今執筆しているC++本で、ふと気がついたら、何を間違えたか、

アグリゲート(aggregate)とは、配列かクラスでユーザー定義のコンストラクター、非staticデータメンバーの初期化子、privateおよびprotectedな非staticデータメンバー、基本クラス、virtual関数が存在しないものをいう。

などと書いていた。これでは、配列にも、ユーザー定義のコンストラクター云々といった条件が課されるように読めてしまう。危ないところだった。このままでは、日本トンデモ本大賞を受賞してしまう。配列は、たとえアグリゲートではないクラスの配列型であったとしても、アグリゲートである。

しかし、一般に、あまりアグリゲートではないクラスの配列をリスト初期化した覚えはない。例えば、以下のようには書かない。

std::string a[] = { "hello", "fake silly C++", "world" } ;

そもそも考えて見れば、ここ数年は、配列を積極的に使った覚えはない。私は普通、以下のように書く。

std::vector< std::string > v = { "hello", "truly awesome C++", "world" } ;

2010-09-21

Liberty Bell

最近、ずっとLiberty Bellを聞いている。Liberty Bellとは、John Philip Sousaによって、作曲された、アメリカの軍隊行進曲である。

この音楽は、Monty Python's Flying Circusのテーマとして使われている。なぜこの曲が使われたかというと、この曲が、番組の内容にまったくもって似つかわしくないためだという。また、この曲はパブリックドメインであったので、ロイヤリティを払わなくてもよいという理由もあったらしい。

リファレンスの初期化

どうも、リファレンスの初期化は、正確に説明しようとすると、難しくなってしまう。かといって、分かりやすさを重視すると、それだけ不正確になってしまう。どうしたものか。

C++の機能の評価は難しい

私は、C++0xの初期化リストが好きではなかった。初期化リストが果たして現実に役に立つのか疑問であった。しかし、実際に初期化リストを使ってみると、なるほど確かに便利だ。

私は、range-based forも好きではなかった。ところが、これも実際に使ってみると、便利であると言わざるを得ない。

私は早くから規格を読んでいたので、初期化リストやrange-based forの文法や意味は、すでに理解していた。しかし、規格を読むのと、実際に動くコードを書いてみるのとは、だいぶ違いがある。どうやら私は、機能の良し悪しを評価する能力に欠けているらしい。

さて、もうひとつ、嫌いな機能が残っている。user defined literalだ。現段階では、user defined literalは使うべきではないと考えている。はたして、これも単なる食わず嫌いなのか。

2010-09-20

コンセプトの経緯

もう、この事に関しては、さんざんに書いたと思うのだが、もう一度書いておく。こんどは、具体的な例を挙げてみよう。

Rangeというコンセプトを考えるとする。ここでは、Rangeというコンセプトを満たす型は、Iterator T::begin(void)、Iterator T::end()というメンバー関数を持ち、それぞれ、先頭、終端のイテレーターを返すものとする、と定義する。もちろん、ネストされた型名も必要だ。

concept Range < typename T >
{
    typename Iterator ;
    Iterator T::begin() ;
    Iterator T::end() ;
}

ところで、もし誰かが、たまたま、全く同じシグネチャのメンバー関数とネストされた型名を持ったクラスを書いたとする。このクラスにおけるbegin()/end()は、イテレーターとは全く関係がない。ただ、シグネチャが偶然一致しただけである。

struct Foo
{
    typedef int Iterator ;
    Iterator begin() { return 0 ; }
    Iterator end() { return 0 ; }
} ;

もし、コンセプトマップが、すべてのクラスに対して自動的に生成されるとするならば、このような、偶然のシグネチャの一致に対しても、自動的にコンセプトマップが生成されてしまう。

その結果、本来ならば、「この型はRangeコンセプトを満たしていないよ」という、分かりやすいコンパイルエラーを出せるはずのコードに、「コンセプトはすべて満たしているけどエラーになった」などという、不思議なエラーメッセージが生成されてしまう。

これを防ぐには、コンセプトマップは自動的に生成されないようにして、もし、そのクラスがコンセプトの要求を本当に満たしているならば、そのクラスに対して、明示的にコンセプトマップを書く必要がある。

しかし、その場合、一見すると無駄な記述をしなければならない。

template < typename T>
concept_map Range< std::vector<T> > { }
template < typename T>
concept_map Range< std::deque<T> > { }
template < typename T>
concept_map Range< std::list<T> > { }

このように、明示的にコンセプトマップを生成しなければならない。中身は空である。というのも、STLのコンテナーの実装は、Rangeを満たしていて、何もマップする必要がないのだから。

コンセプトマップを自動的に生成されないようにすると、あらゆる型に対して、明示的にコンセプトマップを書かなければならない。自動的に生成されるようにすると、たまたまシグネチャが一致した場合に困る。

去年、この問題で大いに揉めた。ある者は、「たとえ空でも、絶対に明示的に宣言させるべきだ」と主張し、またある者は、「理想は結構だが、実質的に空の定義なんて、文法上のゴミと同じじゃないか」と主張した。議論の結果、自動的にコンセプトマップを生成するか、あるいは明示的に生成しなければならないかを、選べるようにしようという提案がなされた。

2009年のFrankfurt会議では、この提案が可決される予定であった。しかし、会議でも大いに揉めたらしい。いつまでたっても意見が一致しなかったので、投票がなされた。ドラフトからコンセプトを削除するという投票であった。あのBjarne Stroustrupも、あのDouglas Gregorも、削除に投票した。結果、コンセプトは削除されることになった。

配列初期化の不思議な機能

多次元配列は、以下のように初期化できる

int main()
{
    int a[2][2] = { 1, 2, 3, 4 } ;
    // a[0][0] = 1, a[0][1] = 2, a[1][0] = 3, a[1][1] = 4

    // 出力
    for ( auto & i : a )
        for ( auto j : i )
            std::cout << j << std::endl ;
}

何故こんな機能があるのか分からない。こんな記述は、非常に分かりづらい。

気がついたら、指が自然にrange-based forを書いていた。あんなにrange-based forを嫌っていたのに、これはどうしたことか。

2010-09-18

range-based forの興味深い使い方を発見した

初期化の部分を執筆中に、ふと、range-based forの面白い使い方を思いついた

template < typename ... Types >
void f( Types ... args )
{
    for ( auto value : { int(args)... } )
    {
        std::cout << value << std::endl ;
    }
}

int main()
{
    f( 1 ) ;
    f( 1, 2, 3 ) ;
    f( 1, 2, 3, 4, 5 ) ;
}

range-based forには、初期化リストを渡せる。初期化リストには、引数パックを使うことができる。ということは、わざわざ再帰的なアルゴリズムを使わずして、Varidic Templatesを使った可変引数をすべてforで回せるのである。int型にキャストしているのは、Variadic Templatesは、それぞれ型が違う可能性があるからである。

ところで、この関数には、ひとつ問題がある。それは、テンプレートパラメーターパックがゼロ個だった場合、実に意味不明なエラーを出すということである。

int main()
{
    f() ; 
}

gccでは、以下のようなエラーがでる。

In function 'void f(Types ...) [with Types = {}]':
   instantiated from here
 error: unable to deduce 'std::initializer_list<_Tp>&&' from '<brace-enclosed initializer list>()'
 error: unable to deduce 'auto' from '<expression error>'

理由は、range-based forに、空の初期化リストを渡せないためである。

for (auto i : { } ) ; // ill-formed

こんなエラーメッセージでは、関数の実装を知らないユーザーは、何が間違っているのかということが理解出来ない。いったいどうすればいいのか。

ゼロ個のparameter packを禁止したい場合なら、方法はいくつでもある。まず、static_assertを使う方法。

template <typename ... Types >
void f( Types ... args )
{
    static_assert( sizeof...(Types) != 0, "zero parameter pack is not allowed" ) ;
    for ( auto value : { int(args)... } ) ;
}

int main() { f() ; }

エラーメッセージは以下の通りである。

In function 'void f(Types ...) [with Types = {}]':
   instantiated from here
 error: static assertion failed: "zero parameter pack is not allowed"
 error: unable to deduce 'std::initializer_list<_Tp>&&' from '<brace-enclosed initializer list>()'
 error: unable to deduce 'auto' from '<expression error>'

相変わらずエラーはでるが、最初にstatic_assertが表示される。

しかし、人間というものは、エラーメッセージを読まない生き物である。あらゆる人種の中でも、最も怠惰な種族であるプログラマが、エラーメッセージなど読むわけがない。ましてや、static_assertでは、basic source characterしか使えない。とすると必然的に英語で欠かなければならず、日本人にやさしくない。もっと分かりやすいエラーにしたい。

引数をひとつ取るという事も考えられる。

template <typename ... Types >
void f( int x, Types ... args )
{
    for ( auto value : { x, int(args)... } ) ;
}

int main() { f( ) ; }

エラーメッセージは、実に分かりやすくなる。

In function 'int main()':
 error: no matching function for call to 'f()'
 note: candidate is: template<class ... Types> void f(int, Types ...)

ただし、そのためだけにわざわざ引数をひとつ、明示的に欠かなければならないのは苦痛だ。

deleted functionを使うという手もある。

template <typename ... Types >
void f( Types ... args )
{
    for ( auto value : { int(args)... } ) ;
}

void f() = delete ;

int main() { f( ) ; }

エラーは実に分かりやすくなる。

In function 'int main()':
 error: use of deleted function 'void f()'

しかし、これらは、ゼロ個のパラメーターパックを禁止してもよい場合の話である。ゼロ個のパラメータパックを使わせたい場合で、もし、ひとつ以上の引数があれば、range-based forを使いたい場合はどうすればいいのか。結局、その場合は、関数のオーバーロードを使って、別々に実装するか、あるいは、テンプレートメタプログラミングを使って、コンパイル時の条件分岐を行うしか方法がない。

range-based forが、空の初期化リストを、特別な場合として受け入れてくれれば、このような苦労はしなくて済むのだが。

追記:

range-based forは空の初期化リストを受け付けるべきではないかということを話したら、もっと簡単な解決方法を提案された。

for ( auto i : std::initializer_list<int>{} ) ;

そうか、こうすればよかったのだ。空の初期化リストは、型が分からないので受け取れないが、初期化リストのオブジェクトならば、受け取れる。

多くのプログラマは言語を表面的な理解だけで使っている

一般のプログラマの多くは、プログラミング言語というものを、ごく浅い表面的な理解だけで使っている。これは、いわゆる「入門書」によるところが大きい。入門書は、言語をできるだけパターンで教えようとする。かくかくしかじかの場合には、とらとらうまうまのように書いておけばいい、などといった具合だ。

たとえば、配列の全要素や、aggregateの全メンバーをゼロで初期化したいとする。多くのC++プログラマは、以下のように書く事であろう。

int a[100] = {0} ;

このコードは、正しく動く。配列aの要素は、すべてゼロで初期化される。しかし、C++という言語を考えた場合、{0}と書く必要はない。空の{}で十分なのである。

int a[100] = {} ;

では何故、多くのC++プログラマは{0}と書くのか。それは、多くの参考書が、そのように書いているからに過ぎない。大多数のC++プログラマは、どうも{0}を、特別な文法か何かのように錯覚しているようである。

そもそも、以下のコードはどういう意味なのか。

T x = {0} ;

これは、「Tの最初の要素あるいはメンバーを、0で初期化し、その他をすべて、staticストレージと同じ方法で初期化する」という意味である。staticストレージは、明示的に初期化子を欠かなくても、必ずzero-initializedされる。そのため、このような意味になる。たとえば以下のように書けば、

int a[3] = {100} ;

「a[0]を100で初期化し、a[1]とa[2]はゼロ初期化する」という意味になる。

しかし、それだったら最初から、すべてstaticストレージと同じ方法で初期化する意味で書けばいいのだ。

int a[1000] = {} ; // すべての要素をstaticストレージと同じ方法で初期化(すなわち、ゼロ初期化)

すでに述べたように、なぜ、いまだに多くのC++プログラマが{0}と書くかというと、多くの参考書がそう書いているからだ。なぜ、多くの参考書がこう書いているかというと、C言語では、空の初期化リストは書けなかったのだ。

int a[5] = { } ; // C言語ではエラー、C++ではOK

これは、C++による改良である。C言語の文法は絶望的に汚い。近年のC言語の文法がいくらかましになっているのは、C++のおかげであると言える。ところが、C99ですら、いまだに空の{}を認めていない。C99は、C++なら、わざわざ言語機能にするほどでもないようなものまで、言語機能にしていたりと、かなり迷走している。また、次のC言語規格も、悲惨である。C言語に未来はない。

配列や構造体の中身をゼロで初期化したいというのは、プログラミングにおいては、かなり一般的な要求であった。そのため、{0}というおまじないのような記法が生まれることになり、あまりに多用されたため、多くのプログラマは、{0}が特殊なルールででもあるかのように錯覚しているのである。

ほとんどのプログラマは、言語を、厳密な規格の定義を読んで理解することなく、信じられないほど簡略化された、参考書によって、表面的に理解している。

たとえばdynamic_castは、polymorphicな基本クラスのポインターやリファレンスを、派生クラスのポインターやリファレンスに、実行時の型情報のチェック付きで変換するという機能を提供している。しかし、dynamic_castは、派生クラスのポインターやリファレンスから、基本クラスのポインターやリファレンスに変換するという機能も提供している。

struct Base { } ;
struct Derived : Base { } ;

int main()
{
    Derived d ;
    Base & b = dynamic_cast< Base & >( d ) ;
}

これは、static_castを使った場合と、全く同じである。実行時チェックも入らない。なぜならば、この型変換は、静的に行うことができるからだ。ある型が派生クラスであるということは、必ず基本クラスでもあるのだ。実行時チェックの入りようがない。しかし、多くのプログラマは、dynamic_castでは、必ず実行時チェックが入るものだと勘違いしている。何故このような機能があるのかというと、結局のところ、dynamic_castは、クラスヒエラルキーのナビゲーションのために使うものであり、一貫性をとるためではなかろうかと思われる。

同様にして、const_castで、CV-qualifierを付け加えることもできるし、まったく同じCV-qualifierにすることもできる。

int main()
{
    int const * ptr = nullptr ;
    const_cast< int const *>( ptr ) ;
}

これも結局、const_castというのは、CV-qualifierの増減に関する機能であり、CV-qualifierを減らすだけしか認めないのであれば、どうも一貫性にかけるのであろう。

ではなぜ、参考書の著者が、もっとも厳密に言語を教えないのかというと、結局、そこまで厳密に知っていても、通常のプログラミングには、あまり役に立たないためであろう。しかし、今日もネット上では、もし規格を数行読んでいたら、疑問の沸きようがない文法上の問題に対して、喧々諤々の論争が繰り広げられている。

追記:

さらに厳密に言うと、C言語とC++では、言語の定義の文面が異なる。対応する初期化リストの要素のないアグリゲートのメンバーは、C言語では、staticストレージと同じ方法で初期化される(つまりゼロ初期化される)。C++では、値初期化(value-initialize)される。値初期化では、アグリゲートとなりうる型はすべて、ゼロ初期化される。

もっとも、これらは単なる定義方法の違いというだけに過ぎない。

2010-09-17

「ぼくたちの洗脳社会」を読んだ

岡田斗司夫著、ぼくたちの洗脳社会を読んだ。これは、1998年に出版された本らしいが、現在、無料でPDF版を落とすことができる。

ぼくたちの洗脳社会 - オタキングex公式サイト

なかなか面白い考えだ。

しかし、どうもあの頃の時代というのが、よく思い出せない。確か、1998年当時、私はすでにインターネットをバリバリに使っていたはずだ。パソコン通信など、とっくに絶滅していたのではあるまいか。ところが、この本では、パソコン通信の例ばかりあげて、インターネットのことは、ほとんど挙げていない。不思議だ。

2010-09-16

constexprはどこへいく

現状のconstexprには、まだまだ問題がある。もっとも、問題があるのは、constexprの定義というよりは、定数式の定義である。何をもって定数式とするかが、なかなか難しい。

constexprと定数式に関わる部分は、まだしばらく執筆できそうにない。

2010-09-15

Google IMEは不思議すぎる

「リベリオン」と入力したところ、「Equilibrium」という変換候補が現れた。これは、同名の有名な拳銃格闘の映画に由来する。原題はEquilibriumだが、邦訳はリベリオンなのだ。

おそらくは、カタカナと英単語の辞書に、登録されているのだろう。これが意図したものでなく、自然に生成されたとすると、ちょっと評価に苦しむ。良いのか悪いのか。なぜならば、本来の「Rebellion」という変換候補は現れないからだ。

リベリオン - Wikipedia

2010-09-14

auto指定子の落とし穴

auto指定子では、他の指定子や宣言子を組み合わせることができる。

int x = 0 ; 
int & ref = x ;

ここで、宣言子がrvalueリファレンスの場合、注意を要する。

int main()
{
    int x = 0 ;

    int && r1 = x ; // エラー、rvalueリファレンスをlvalueで初期化できない
    auto && r2 = x ; // OK、ただし、r2の型はint &
    auto && r3 = std::move(x) ; // OK、r3の型はint &&

    // false
    std::is_rvalue_reference< decltype(r2) >::value ;
    // true
    std::is_rvalue_reference< decltype(r3) >::value ;
}

なぜならば、auto指定子の型は、template argument deductionと同じルールで決定されるからである。

template < typename U >
void f( U && u ) { }

int main()
{
    int x = 0 ;

    f( x ) ; // Uはint &
    f( std::move(x) ) ; // Uはint &&
}

これは気がついてよかった。参考書にも書いておかなければ。

rangeをどう訳すか

range-based forを解説する上で、決定しなければならないことがひとつある。どう訳すかだ。

for statementは、for文とか、あるいは単にforという名称が定着している。では、range-based forはどうするか。単に、range-based forと書いて、日本人に通じるであろうか。

さらに、コンセプトとしてのrangeはどう訳すべきなのか。「範囲」という言葉は、あまりにもジェネリックすぎて、訳がわからない。iteratorに対してイテレーターが定着しているように、rangeに対しても、レンジが通じるだろうか。

range-based forの見た目は簡単だが、詳細はかなり複雑だ。range-based forを真に理解するためには、ADLとイテレーターを理解しなければならない。

gccにrange-based forが実装された

gccにrange-based forが実装されたので、検証した。バグは見つからなかった。残念。Rodrigo Rivas Costaはいい仕事をしたと言える。

さて、そろそろrange-based forの説明も執筆しなければならないのだが、さてどうするか。range-based forを理解するには、ADLを理解していなければならない。また、ライブラリとしてのイテレーターの仕組みも、理解していなければならない。range-based forは、非常に簡単にループが書けるので、初心者におすすめなのだが、いかんせん、その仕組が難しい。本末転倒も甚だしい。

そういう理由で、私はrange-based forを嫌っていたのだが、いざ実際に使ってみると、これがなかなか便利だ。うーむ。

namespace NS
{
    struct Member_range
    {
        int a[3] ;
        Member_range() : a{ 1, 2, 3 }
        { }
        
        int * begin() { return &a[0] ; }
        int * end() { return &a[3] ; }
    } ;

    struct Non_member_range
    {
        int a[3] ;
        Non_member_range() : a{ 1, 2, 3 }
        { }
    } ;
    
    int * begin( Non_member_range & r ) { return &r.a[0] ; }
    int * end( Non_member_range & r ) { return &r.a[3] ; }
}


int main()
{
    // initializer list
    for ( auto i : { 1, 2, 3 } )
    { std::cout << i << std::endl ; }

    // array
    int a[] = { 1, 2, 3 } ;
    for ( auto i : a )
    { std::cout << i << std::endl ; }

    // range aware class
    NS::Member_range r1;
    for ( auto i : r1 )
    { std::cout << i << std::endl ; }

    // class with associated range functions
    NS::Non_member_range r2 ;
    for ( auto i : r2 )
    { std::cout << i << std::endl ; }
    
}

Public Git Hosting - official-gcc.git/commit
GCC 4.6 Release Series — Changes, New Features, and Fixes - GNU Project - Free Software Foundation (FSF)

constexprがわけわからないレベルに達している

もうconstexprについていく自信がない。現在、文面にバグがあるので議論されている最中だが、どうも、本来の意図する定義では、以下のようなことができるはずである。

constexpr int f() { return 1 ; }
constexpr int g() { return 2 ; }
constexpr int h() { return 3 ; }

constexpr int (*table[])(void) = { &f, &g, &h } ;

constexpr int call( int i )
{
   return table[i]() ;
}

これは、ジャンプテーブルをコンパイル時に構築して、コンパイル時に実行するコードである。

また、initializer_listを、constexpr対応させようという意見もある。現在、initializer_listは、constexpr対応ではないが、これをconstexpr対応にしようというのだ。

constexpr int f( std::initializer_list<int> list )
{
    return *list.begin() ;
}

int main()
{
    constexpr int value = f( { 1, 2, 3 } ) ; // 1
}

もうついていけそうにない。

2010-09-11

Google scribe

Google Scribe

This is a Google Scribe. It's Google lab service. It offers a suggestion feature for English. So I can save types and prevent typos.

What it makes so good is, that I can use it in any input form by using a bookmarklet. For example, I am writing this sentence enabling Google Scribe.

Although I like the idea of this feature, it's rather annoying for now. Because it's a bit slow so it can't keep up with my typing. I type some words, and seconds later, it shows candidates. Too late.

initializerの説明が難しい

initializerの説明が難しい。一体どうすればいいのか。

とりあえず、規格にしたがって、zero-initialize、default-initialize、value-initializeの説明を書いた。これは、まだ分かりやすい。

初期化子の文法も分かりにくい。基本的には、三つある。

1. 初期化子を書かない。

T x ; 

2. = に続けて式。

T x = a ;

この形の初期化子を、コピー初期化(copy-initialization)という。ただし、ここでのコピーは、コピー、ムーブのコピーとは関係がない。コピー初期化でも、ムーブはされる。

3. コンストラクター呼び出しに似た形。

T x(a) ;
T x{a} ;

この形の初期化子を、直接初期化(direct-initialization)という。

とこう書くと、非常に種類が少なく、分かりやすいように思われる。しかし、以下の形の初期化子は、別物のように見えてしまう。

T x = { 1, 2, 3 } ;
T x( { 1, 2, 3 } ) ;

一番難しいのが、初期化がどのように行われるかという具体的な決定方法だ。これは、非常に複雑だ。ユーザー定義の変換関数やオーバーロード解決も絡んでくるためだ

問題は、この初期化の仕組を詳細に解説しても、あまり現実のプログラミングには役に立たないのだ。初期化の仕組みは、詳細なルールを知らなくても、普通のユーザーが普通に使えるように設計されている。もちろん、いくつか落とし穴はあるが。

2010-09-10

aggregateと初期化リストの不思議

ちょうど今、initializerの項目を執筆している。この部分は、結構難しい。分かりやすく説明しようとすれば、不正確になってしまうし、規格に忠実であることを求めると、規格のように無味乾燥とした、正しいが分かりにくい文章になってしまう。

このため、なかなか執筆が進まないのだが、このままではいけないので、ともかくこのブログで、何か解説をしてみようと思う。

たまたま2chのスレで、aggregateの話題が出ているので、これについて、なかなか複雑な部分を、解説する。

C言語では、配列や構造体(C言語の用語)を初期化リストで初期化できた。

struct Foo { int x ; int y ; } ;
struct Foo foo = { 1, 2 } ;

同じことは、C++でもできる。ただし、C++には「構造体」というものはない。すべて、クラスである。C++では、ある特殊な制限を満たした配列とクラスのことを、aggregate(アーグリゲート)という。aggregateは、初期化リストで初期化できる。

aggregateであるためには、配列かクラスで、
ユーザーによって提供されたコンストラクターがないこと、
非staticデータメンバーに初期化子がないこと、
privateやprotectedな非staticデータメンバーがないこと(言い換えれば、publicのみ)、
基本クラスがないこと、
仮想関数がないこと、
という条件を満たす必要がある。

このうち、「非staticデータメンバーに初期化子がないこと」という条件だけは、C++0xからの新機能であるので、解説が必要である。C++では、非staticなデータメンバーにも、初期化子を書く事ができる。

struct Foo
{
    int x = 0 ;
    Foo() { } 
    Foo( int x ) : x(x) { }
} ;

Foo a ; // a.x = 0
Foo b(1) ; // b.x = 1

このように、非staticなデータメンバーに初期化子を書くと、コンストラクターのメンバー初期化が省略された場合、その初期化子で初期化される。

ということは、当然、ユーザーによって提供されたコンストラクターがあるクラスは、aggregateではないので、C言語のような初期化はできない。

struct S
{
    S() { }
    int x ;
} ;

S s = { 0 } ; // エラー

ところで、このaggregateの定義には、ひとつ疑問がある。非staticなデータメンバーがどのようなクラスであるかについては、何も言及していないのである。もし、データメンバーも、同じ条件を見做さなければならないとすれば、aggregateではないデータメンバーを持たない、などといった文面があるはずである。しかし、そのような文面はない。したがって、非staticなデータメンバーは、コンストラクターも持てれば、アクセス指定子も使えるし、仮想関数も使えるはずである。

struct A
{
    int x ;
    struct B
    {
        B(int, int) { }
    } b;
} ;

int main()
{
    A a = { 0, {0, 0} } ;
}

実は、この文面は、非staticデータメンバーの初期化子を除けば、C++98から全く変わっていない。したがって、クラスAは、C++98でもaggregateである。したがって、このコードは、規格上疑いようもなくwell-formedである。

gccは、このコードを通す。MSVCは、「'A::b' : non-aggregates cannot be initialized with initializer list」などというエラーを吐く。これは、規格上間違っている。

またひとつ、MSVCを使うべきではない理由を発見したわけだ。この分では、C++0xの初期化リストの対応も、あまり期待できそうにはない。

次に、よく、構造体や配列を、ゼロで初期化するのに、以下のような記述が使われる。

int a[100] = {0} ;

これは、C言語では正しい。しかし。C++では、何もこのように書く必要はない。

C言語では、空の初期化リスト、{}は書けない。C言語は、その文法上、必ず何かひとつは、初期化リストに式が入っていなければならない。そして、初期化リストによって初期化する際に、配列や構造体のメンバーに、対応する初期化リストの値がない場合、staticストレージと同じように初期化される。staticストレージは、必ずゼロで初期化されることが保証されている。したがって、上記のコードは、「a[0]を0で初期化し、残りの要素をstaticストレージと同じ方法で初期化せよ」という意味である。

C++では、空の初期化リストを書く事ができるようになった。そこで、上記のコードは、以下のように書ける。

int a[100] = {} ;

これは、「配列のすべての要素をstaticストレージと同じ方法で初期化せよ」という意味である。

もちろん、まともなコンパイラーならば、実際に生成されるコードに違いはないだろう。しかし、C++の方が分かりやすい。これはいい改良である。

しかし、ほぼすべての参考書で、配列の要素や構造体のメンバーをすべてゼロで初期化するには、{0}で初期化すればよいという記述がなされている。そこで、現実のC++のコードも、多くは{0}を使っている。思うに、一部の参考書の筆者も、{0}とはどういう意味なのかということを、まともに考えたことがないのではあるまいか。ただ、{0}を、特別な文法か何かのように考えているのではあるまいか。

ちなみに、C++0xでは、リスト初期化がプログラマにも提供されたため、aggregate以外でも、リスト初期化できる。これには通常、std::initializer_listを使う。しかし、すばらしいことに、通常のコンストラクターも考慮される。つまり、以下のように書ける。

struct S
{
    S(int, int, int) { }
} ;

S s = { 1, 2, 3 } ;

追記:よく読んだら、C++98およびC++03には、コンストラクターを考慮するという機能がないので、

struct A
{
    int x ;
    struct B
    {
        B(int, int) { }
    } b;
} ;

このクラスは、aggregateだが、初期化リストでは初期化できないということになる。

2010-09-09

関空、成田間五千円の飛行機

関西空港と成田空港の間を、五千円で移動する空の便ができる予定らしい。おお、これなら格安で東京に日帰りできるのではないか。夜行バスを使っても、やはり往復で九千円ぐらいかかるのだ。片道五千円なら、同じ程度だ。しかもこちらは飛行機、所要時間は、二時間弱である。と思ったが、どうも、うまくは行かないようだ。

まず、私は京都に住んでいる。飛行機で東京まで行く場合、関西空港まで行かなければならない。ところが、この関西空港というのは、大阪の都市部からは、だいぶ離れている。はっきり言うと、ド田舎である。電車にせよ、直通バスにせよ、京都から関西空港まで、一時間半はかかるだろう。もちろん、運賃も千円以上かかる。

通常、私が「東京」へ行くという場合、それは確実に、「東京都区部」、または、「東京23区」と呼ばれる、東京のど真ん中に用事がある。ところが、成田空港というのは、千葉県のド田舎にある。ここから23区まで電車で行こうとすると、やはり一時間ぐらいの時間がかかるし、運賃も千円以上かかる。

これなら、新幹線の自由席に乗ったほうが速いし、余計な乗換を考えずに済むし、トータルの運賃も、さほど変わらないのではないのではないか。

結局、空港というのが、様々な理由で、大抵がド田舎に作られているために、関東、関西間の交通には、あまり役に立たないのだろう。北海道や沖縄と、本州との間を行き来するのでもなければ、新幹線に劣る。

2010-09-07

weirdは由緒正しき言葉

weirdという英単語がある。私は、今まで特に理由もなく、直感だけで、weirdは、比較的最近の言葉だろうと考えていた。スラングではないかとさえ思っていた。だいたい、響きからしてweirdではないか。ところが、これが違うのだ。

weirdという言葉は、西暦900年以前にまでさかのぼれる、由緒正しき言葉である。元はといえば、バイキングが、「運命」という意味で使っていたらしい。

Runecasting - Runic Divination
Wyrd - Wikipedia, the free encyclopedia

現在、weirdという言葉は、「奇妙な」などという意味で使われる。これは、1815年に用例が確認されている。意味が定着したのは、20世紀に入ってからであるとされている。

とすれば、比較的最近の言葉、もしかしたらスラングではないかという、私の言語的な直感は、それほど間違ってはいなかったということになる。

ちなみに、何故そのような直感があったかというと、weirdが使われている文脈は、いずれも口語的な英語であり、堅い英語では、用例をみなかったからだ。Weird Al Yankovicの存在も大きいだろう。

ゲーム専用機の歴史

ふとしたことから、歴代のゲーム専用機用のゲームソフトの、総売上数や総出荷数のランキングを掲載しているWebサイトを見つけた。

★ゲームデータ博物館★

非常に興味深い情報である。私が遊んだことのあるゲーム機で、一番最新のものは、ゲームボーイカラーであり、最近のゲーム専用機の事情については全くわからない。私は、自由にプログラミングできないハードなど欲しくはないので、最近のゲーム専用機は持っていない。最先端のゲームを離れること久しいので、ついにゲーム自体をやらなくなってしまった。しかし、この情報があまりにも面白かったので、これを元にゲームのソフトに関する歴史の考察をしてみたいと思う。ハードウェアその他の歴史については、Wikipediaに詳しいので、それを参照して欲しい。また、このサイトには載っていないゲーム機もあるが、それは省略する。

ファミリーコンピューターは、1983年に登場した。
ファミリーコンピューターのランキング
ファミリーコンピュータ - Wikipedia

ファミリーコンピューターの売上、一、二位は、マリオである。三、四位に、ドラクエが入ってくる。その下は、ゴルフ、ベースボール、麻雀といった、汎用的なソフトが多い。たまに、テトリスやゼルダの伝説のようなゲームが、ちらほら出現するぐらいである。

ファミコンのアクションゲームは、非常にシビアなアクション性を求めるものが多かった。また、ボタンの連打速度を求めるものも多かった。RPGでは、ゲーム中に何のヒントもなく、事前に知っているか、偶然や勘に頼るか、総当りに調べなければ解けないような謎解きが多かった。結局、ハードの性能の問題から、あまり長いゲームを作ることができず、長くゲームを遊べるように、このような方法が取られたのであろう。また、当時の家庭用ゲームは、まだアーケードの影響を強く受けていた。アーケードは、ある程度ユーザーをゲームオーバーにしなければならないゲームである。さもなければ、一クレジットで延々と遊べてしまうからである。そのため、ファミコンのゲームも、ゲームオーバーとなれば最初からというゲームが、非常に多かった。まだ、家庭用ゲームとアーケードの違いが、明確ではなかった頃の話である。

スーパーファミコンは、1990年に登場した。
スーパーファミコンのランキング
スーパーファミコン - Wikipedia

スーパーファミコンのソフト出荷の上位は、マリオとドンキーコング、ドラクエとファイナルファンタジーで占められている。現在でも続編が作られ続けている有名ゲームの多くは、スーパーファミコンから始まったものも多い。スーパーファミコンは、10年の長きに渡って、あまりにも大量のソフトが開発されたので、あらゆるジャンル、難易度のゲームが作られており、一般論を述べるのは不可能に近い。ただし、年を経るごとに、ファミコンのような理不尽にシビアなゲームは減り、家庭用ゲームとアーケードの区別が明確になってきている。

ゲームボーイは、1989年に登場した。ゲームボーイカラーは、1998年に登場した。 ゲームボーイのランキング
ゲームボーイ - Wikipedia
ゲームボーイカラー - Wikipedia

1989年~1995年のランキングをみると、ほとんどアクションゲームである。テトリス、マリオ、カービィ、いずれもアクション性に特化したゲームである。この頃のゲームボーイは、アクションゲームが全盛であった。ただし、貧弱なハードのため、操作に支障が生じるゲームも多かった。悪魔城ドラキュラなどがいい例だ。

1996年~2002年のランキングでは、アクションゲームがすっかり消えてしまった。ポケモンの影響があまりにも強かったのだろう。ポケモン、遊戯王、メダロット、いずれもアクション性が皆無のゲームである。ゲームボーイ(カラー)用のソフトは、アクション性の低いRPGないしは、やり込みゲームになった。ひたすらレベル上げやアイテム収集を楽しむゲームが多い。処理能力は低いが、どこにでも手軽に持ち運べ、他人と通信できるというゲームボーイという特性を考えれば、やり込み性の高いゲームの方が向いているのは、当然である。

プレイステーションは、1994年に登場した。
プレイステーションのランキング
プレイステーション - Wikipedia

プレイステーションの売上の一位から四位までは、ドラクエとFFといったRPGによって占められている。その下に、グランツーリスモやバイオハザードといった、アクションゲームが続く。プレイステーションは、非常に息の長いハードなので、多くのジャンルのソフトがでており、あまりジャンルの偏りはみられない。家庭用ゲームにおける3Dゲームは、プレステが火付け役となったといってもいいかもしれない。

ネオジオポケットとワンダースワンは、そもそも売上自体が少ない。当時を振り返るに、かなりマイナーなハードだったと思う。ネオジオポケットの売上の上位は、格ゲーで占められている。ワンダースワンは、グンペイという興味深いゲームはあったものの、ほとんどのソフトが、移植作で占められている。

セガサターンとドリームキャストも、マイナーなハードであった。ハードの登場には、4年の開きがあるので、まとめて語るのもどうかと思うが、結局、どちらもプレスに勝てなかったと言わざるをえない。ただし、この二つのハードには、興味深い異色作が多かった。セガCDという拡張機器によって、ゲームにアニメーションを入れたりしていたし、ドリームキャストでは、シーマンという異色なゲームもあった。あるいは、マイナーであったからこそ、異色作が目立つのであろうか。

ニンテンドー64は、1996年に登場した。
ニンテンドー64のランキング
NINTENDO64 - Wikipedia

ニンテンドー64も、やはり、プレステに勝てなかったハードである。売上の上位は、ほぼアクションゲームで占められている。マリオのゲームが非常に多い。また、任天堂名義のソフトが、非常に多い。任天堂ハードはサードパーティが勝てないというのは、よく言われることであるが、ここまで圧倒的に任天堂で占められているのも異常である。たとえ、開発は下請けだとしてもだ。

プレイステーション2は、2000年に登場した。
プレイステーション2のランキング
プレイステーション2 - Wikipedia

プレステ2は、かなり描画能力が向上した。2010年の時点で、いまだにPS2用のソフトが開発されている。まだ現役のハードなのだ。肝心のソフトはどうなのかというと、売上の上位には、有名ソフトの続編が多いように思われる。これは、描画能力の上昇に伴い、開発費も高騰したという事情が関わっている。開発費が非常に高くつくため、絶対に売れると確信できるゲームが、多く開発された。そのため、続編だらけである。ハードの最大の性能を引き出すには、異常な努力が必要であるという点からも、開発費は高くつく。ただし、最近はノウハウが蓄積されていて、使いやすいライブラリなども開発されているらしい。

ゲームキューブは、2001年に登場した。
ゲームキューブのランキング
ニンテンドーゲームキューブ - Wikipedia

ゲームキューブは、かなりソフトの開発しやすい環境であったと言われている。高いピーク性能より、平均的に性能を発揮できるハードであった。メモリのレイテンシも低い。ただし、PS2に勝てなかった。ニンテンドー64と同じく、売上の上位には、アクション性の高いゲームが多いように思われる。もっとも、ゲームキューブには、単にゲームを攻略するだけならば、シビアなアクション性を求めるソフトは少ないのだが。

ゲームボーイアドバンスは、2001年に登場した。
ゲームボーイアドバンスのランキング
ゲームボーイアドバンス - Wikipedia

売上の上位は、ポケモンで占められている。ただし、アドバンスには、アクションゲームも多い。ハードの性能が向上したことで、アクション性の高いゲームも作れるようになったのだろう。ただし、わずか三年後にニンテンドーDSが登場したため、アドバンスは、どうもマイナーな部類に属するハードではないかと思う。

XBoxは、2002年に登場した。
XBoxのランキング
Xbox - Wikipedia

少なくとも、日本では不振であった。ゲームの売上を見ても、コアなゲームが多い。また、売上数も、圧倒的に少ない。一位以外のソフトは、百万本の桁に到達していないのだ。また、後継機であるXBox360が、早くに登場したため、もはや現役ではない。

ニンテンドーDSは、2004年に登場した。
ニンテンドーDSのランキング
ニンテンドーDS - Wikipedia

ニンテンドーDSは、タッチパネルの採用により、それまでゲームをしていなかった人たちにも、広く受け入れられた。売上の上位に入っている特徴的なソフトとしては、教育ソフトがある。脳トレであるとか、英語や国語、料理などといった、今までにないソフトが、売上の上位に登場した。このような教育ソフトは、ファミコンでも多く作られていたが、売上の上位に食い込むことはなかった。ニンテンドーDSは、教育ソフトを売れるようにしたのである。今までゲームをしてこなかった層を取り込むというのは、ソフト全体の売上を底上げするにも、相当に力のあることである。面白いソフトとしては、著名な文学作品を収録したソフトなどもある。もちろん、既存のアクション性の高いゲームや、やり込み要素の高いゲームも、数多く開発されている。

プレイステーション・ポータブルは、2004年に登場した。
PSPのランキング
プレイステーション・ポータブル - Wikipedia

PSPは、ポータブル機としては相当に高い処理能力を有する。また、音楽や動画の再生、ブラウザ、ワンセグなどの機能も備えている。これらは、ニンテンドーDSでも、ある程度はサポートされているが、PSPは高い処理能力に支えられ、DSよりも質が高い。ただし、DSに勝てなかった。当時の私の印象では、PSPの人気は、ソフトとしての人気というよりも、そのハードの人気であったように思う。私の知るPSPの所有者は、必ずといってよいほど、非公式のカスタムファームウェアを使っていた。目的は、自作のソフトを走らせたり、ソフトウェアによる無意味な制限を取り払うためであった。高い処理能力にもかかわらず、安価であるという点が、一部のコアなオタクに、非常に受けたようだ。しかし、ハードが安いのは、ソフトのロイヤリティに頼っているためであり、本来の意図した使われ方ではなかっただろう。

肝心のソフトはどうかというと、グラフィックに優れたソフトが多いように思われる。また、プレステからの移植作も多い。

XBox360とPS3は、ハードの性能がかなり上がった。ただし、開発費も高騰し、まともにソフトを開発できるのは、相当に力のある企業だけになってしまった。売上の上位は、ほぼ既存の有名タイトルの続編で占められている。また、XBox360、PS3、PCのマルチプラットフォーム向けにゲームが開発されることが多くなった。

Wiiは、XBox360とPS3とは、だいぶ違う路線を走っている。その独特のコントローラーは、三次元に自由に動かすことができ、既存のコントローラーとはだいぶ違ったものとなっている。ゲームも、コントローラーを意識したものが多い。

未来のゲームはどうなるであろうか。ハードを考える。現在、XBox360やPS3は、Wiiに似たコントローラーを出す動きが見られる。また、全身の動きを認識するコントローラーも発表されている。また、立体視のできるディスプレイに対応したソフトの動きも見られる。

ただ思うに、ここ数年、ゲーム専用機の処理能力は、PCに比べて、だいぶ遅れてきているように思われる。

ソフトを考える。ここ最近、全く新しい画期的なソフトというのは、出ていない。あまりにアクション性のシビアなゲームは、もはや流行らない。ゲーム内のメモリ上のデータを変更し続ける、やり込み系のソフトも、何が面白いのかよく分からない。また、多くのゲームが、乱数に頼っている。多くのゲームが、何度もやり続けることで、レアアイテムなどを得るようになっている。一体、擬似乱数のアルゴリズムと格闘して何が楽しいのであろうか。理解出来ない。開発費の高騰から、続編や移植ばかりが開発されるようになっている。しかも、売上の上位に、そういったソフトが上がるので、なお悪い。ゲームの未来は、あまり明るくないように思われる。

2010-09-05

deleted definitionによるクラスの初期化の制御

bool型で初期化したいクラスがあるとする。

struct Boolen
{
    Boolen( bool ) { }
} ;

残念ながら、このクラスは、あまり宜しくない。なぜならば、C++には、忌々しい暗黙の型変換というものがあるからだ。

int main()
{
    Boolen a = true ; // OK、当然だ
    Boolen b = 123 ; // OK、ハァ?
    Boolen c = &a ; // OK、おいおい、おかしいだろ常識的に考えて
}

このような馬鹿げたコードは、コンパイルエラーになって欲しい。もし、数値やポインターをboolとして扱いたいのであれば、明示的にキャストするべきなのだ。

int main()
{
    Boolen a = true ; // OK、当然だ
    Boolen b = bool( 123 ) ; // 自分が何をしているのかは十分承知している
    Boolen c = bool( &a ) ; // 時にはこんな汚いコードも必要なのだ
}

しかし、現実は汚い。というのも、数値やポインターからboolへの変換というのは、あまりにも基本的すぎる暗黙の型変換であり、到底C++から取り除く事はできないのだ。多くのコンパイラーは、警告さえ発しない。というのも、あまりにも根本的すぎる暗黙の型変換のため、心ないプログラマーによって欠かれたコードに対して、警告を出すと、あまりに大量の警告がでてしまい、警告の意味をなさなくなってしまうからである。結果として、コンパイラーが文句を言わないため、世のクソプログラマーは、このような不自然極まりないコードに対しても、明示的なキャストを書かない。

このきたならしい阿呆がァーーッ!!

そういう輩は、痛い目を見る必要がある。幸い、C++0xには、このようなきたならしい阿呆に痛い目をみせることのできる機能が追加された。deleted definitionsである。この機能は、定義を削除する機能である。定義を削除された関数は、宣言以外の用途で使うことはできない。

おれは人間をやめるぞ!
ジョジョーーーーッ!!
おれは人間を超越するッ!

struct Boolen
{
    Boolen( ) = delete ;// これは無くてもよい
    Boolen( bool ) { }

    template < typename T >
    Boolen( T ) = delete ;
} ;

int main()
{
    Boolen a = true ; // OK
    Boolen b = 0 ; // エラー
    Boolen c = nullptr ; // エラー
}

これは、厳密に言えば、暗黙の型変換を禁止するコードではない。このように書けば、bool以外の型での初期化は、テンプレートのインスタンス化とオーバーロード解決により、関数テンプレート版のコンストラクターが選ばれる。しかし、関数テンプレート版のコンストラクターの定義は削除されているので、使うとエラーになる。結果として、暗黙の型変換を禁止している。

この程度のことは、C++0xならば、できて当然である。

空気を吸って吐くことのように!
HBの鉛筆をベキッ!と
へし折る事と同じようにッ
できて当然と思うことですじゃ!

この、使うとエラーになるというのは、宣言以外のあらゆる利用に該当する。関数の呼び出し、ポインターやリファレンスの取得はもちろん、たとえ未評価式の中でも、変わることがない。

// 削除された関数の定義
void f() = delete ;

void f() ; // OK、宣言はできる

int main()
{
    f() ; // エラー、削除された関数の呼び出し
    &f ; // エラー、削除された関数のポインターを得ようとしている
    void (& ref )(void) = f ; // エラー、削除された関数のリファレンスを得ようとしている
    typedef decltype(f) type ; // エラー、未評価式の中で、削除された関数を参照している
}

ちなみに、現行のgccにはバグがあり、なぜか再宣言すると、エラーの内容が、「削除された関数を使っている」から、「定義がない」というものに変わってしまう。また、最後の例が、エラーなくコンパイル出来てしまう。この問題はすでに報告した。

deleted definitionは、実に面白い。

興味深い変更点

最新のドラフトであるN3126では、14.7.3 Explicit specialization [temp.expl.spec]から、non-deletedという言葉が削除されている。これはつまり、以下のようなコードが書けるということを意味する。

// あらゆるインスタンス化の定義を削除
template < typename T >
void g( T ) = delete ;

// 削除されていない定義
template < >
void g< double >( double ) { }

void call_g()
{
    g( 0 ) ; // エラー
    g( true ) ; // エラー
    g( 0.0 ) ; // OK   
}

関数gは、double以外の型でinstanciateされると、エラーとなる。

以前の文面では、deleted functionを明示的に特殊化することはできなかった。そのため、同じ事をしようとすれば、非テンプレートなオーバーロード関数を書くしかなかっただろう。C++0xでは、関数の明示的な特殊化ができる。そして、別にプライマリーテンプレートではdeletedな定義を、特殊化で定義するのは、特に問題はないと思われる。結局、同じことは非テンプレートなオーバーロード関数で行えるのだ。

あの文面

C++ Standard Core Language Active Issues

cv-qualifierに複数形のsが付いていないことと、top-levelという言葉が使われていないことに関して、issueがすでに存在した。一ヶ月前のことだ。

このissueは、すでに、Status: readyになっているので、次のBatavia会議で承認されるだろう。

deleteは、全く問題ない言葉であるらしい。

issueは一応みているのだが、気がつかなかった。issue listが検索しづらいのは、どうにかならないものか。

それにしても、何か問題を発見したと思ったら、それはすでにissueにあるというケースが多い。もう何度もあるので、最近はだいぶissueも確認しているのだが、何しろissueは膨大なので、把握するのは難しい。

ref-qualifierの有無でオーバーロードはできない

C++0xには、implicit object parameter(*thisが参照するオブジェクト)を、lvalueリファレンスで受けるか、rvalueリファレンスで受けるかを指定する機能がある。これを、リファレンス修飾子(ref-qualifier)という。

struct S
{
    // void S::f(void) &
    void f() & ;
    // void S::f(void) &&
    void f() && ;

    // リファレンス修飾子が省略された関数
    // thisの参照先はlvalueリファレンス
    void g() ;
} ;

int main()
{
    S s ;
    s.f() ; // void S::f(void) &を呼ぶ
    std::move(s).f() ; // void S::f(void) &&を呼ぶ
}

リファレンス修飾子の有無は、関数の型に影響する。ここでは、void f() & ;と、void g() ;は、意味的には同じだが、違う型である。ただし、リファレンス修飾子の有無によって、オーバーロードをすることはできない。

struct S
{
    // void S::f(void) &
    void f() & ; 
    // void S::f(void) &&
    void f() && ;

    // エラー、
    void f() ;
} ;

もし、ある関数名にリファレンス修飾子を書く場合、その関数名のオーバーロード関数には、すべてリファレンス修飾子が書かれていなければならない。あるオーバーロード関数でリファレンス修飾子を省略するのであれば、どのオーバーロード関数にも、リファレンス修飾子を書いてはならない。

残念ながら、リファレンス修飾子を実装しているコンパイラーは、まだこの世に存在しない。

やはりわからない文面

前回の投稿、本の虫: よく分からない文面

やはりわからない。そもそも、cv-qualifierは複数存在こともある。

void f( int const volatile ) ;

しかし、文面は、"Any cv-qualifier..."である。cv-qualifiersではない。とすると、この場合、constかvolatileのどちらかひとつだけが取り除かれるのだろうか。しかし思うに、この場合の関数fの型は、void (int)であるはずだ。とすれば、複数のcv-qualifier(s)を消し去っているわけだ。これまた疑問である。

やはり思うに、Any cv-qualifier modifying a parameter typeとは、any top-level cv-qualifiersと同じ意味ではなかろうかと思う。ではなぜそう書かないのかというと、これまた分からない。

さらに分からないのは、なぜここだけ、deleteという言葉を使っているのかということだ。普通、cv-qualifiersと関係する他の場所では、ignoreとかremoveという言葉を使っている。deleteという言葉は、このような取り除くという意味では、プリプロセッサーあたりでしか使われていない。

2010-09-04

よく分からない文面

関数の型を決定するために、仮引数リストの型には、変換が加えられる。その変換の一つに、

8.3.5 Functions [dcl.fct] paragraph 5

"Any cv-qualifier modifying a parameter type is deleted."

anyとはどういう事だろう。

void f( int const * const ) ;

という関数の宣言があるとする。引数の型は、int const * constである。ここから、any cv-qualifierをdeleteすると、int *である。とすれば、関数の型はvoid ( int * )となるのだろうか。いや、実際は、void ( int const * )となる。とすれば、anyというのは解せない。

似たような他の文面はどうなっているだろうか。typeidも、一部のconstを無視する。

5.2.8 Type identification [expr.typeid] paragraph 5

The top-level cv-qualifiers of the glvalue expression or the type-id that is the operand of typeid are always ignored.

ここでは、top-levelのcv-qualifier(複数可)が無視されると書いてある。

さて、もう一度文面を見直すと、Any cv-qualifierと書いてある。まてよ、名詞に複数形を表すsが付いていない。とすれば、ひとつだけ消し去るという意味であろうか。しかし、anyとあるからには、複数あるうちの、どれを消し去っても問題はないはずである。それに、top-levelのcv-qualifierにしたって、複数である。そんな問題ではないと思う。

解せない。あるいは、paremeter typeというのは、識別子の前までの完全な型名をすべてひっくるめていうのだろうか。つまり、完全な型名をTとして、もし、parameter typeが、Tに対してcv-qualifierを適用しているならば、それを消し去るという意味だろうか。

やはり、どうもそう考えるしかなさそうである。parameter typeは、decl-specifier-seq と declaratorとで決定されるとある。つまり、parameter typeをTとしたとき、もし、void f(T const parameter)となっている場合に、このconstが消されるのだろうか。

規格では、modify typeという表現は、二箇所しかみられない。ここと、8 Declarators [dcl.decl] paragraph 2にある、

The declarators specify the names of these entities and (optionally) modify the type of the specifiers

だけである。

2010-09-03

I Just Called To Say I Love You

昨日、近所のSHOP99に言ったところ、Stevie Wonderの"I Just Called To Say I Love You"が流れていた。

実に単純な歌詞である。「今日は別に特別な日じゃないけれど、ただなんとなく電話をかけて、I love youと言いたかったんだ」という意味の言葉を繰り返すだけの歌である。それだけで、揺ぎない名歌である。

私はつくづく、生まれる時代を間違ってしまった人間である。最近、コンビニでは、実に聴くに耐えぬ騒音を垂れ流している。実際、最近の歌ほど、聴いていて気分が悪くなる歌もないものだ。一体、1990年代、2000年代に何があったのだろうか。何故歌がこれほどにもクソになってしまったのか。

CDが売れなくなった原因として、いわゆる音楽制作、販売としてのレコード会社は、様々な理由を挙げている。曰く、不景気、趣味の多様化、ダウンロード販売、違法ダウンロードと。これらは、それぞれ、それなりの理由があるのだろう。しかし、音楽そのものがクソになったという声は、当のレコード会社からは聞こえてこない。不思議なことだ。まあ、当の音楽制作の本人が、自分の音楽をしてクソだというわけはないか。

興味深いリンク集

なんだか今日は、面白いネタを発見することが多い日だ。

レーベル運営の悲喜交々:HMV渋谷閉店にまつわる僕の見解 - livedoor Blog(ブログ)

音楽に興味はないが、この文章はなかなか面白かった。

Cookie 今昔物語 - はてなるせだいあり

互換性は何事にもおいても優先されるべきだという教訓。

スナイパーライフルが届いたわけだが…おい�... on Twitpic

その綺麗な顔を(ry

出版の現状をまとめた記事

出版状況クロニクル28(2010年8月1日~8月31日) - 出版・読書メモランダム

これは興味深い。もはや、紙の本の時代は終わりつつあるのだろうか。つまり、もはや誰も、紙の手紙を使わなくなったように、紙の本も読まれなくなってしまうのだろうか。現在、Eメールは、単にメールと呼ばれている。とすれば、ebookも単にbookと呼ばれる時代が来るのかもしれない。

紙の本が終わっても別に構わないのだが、現状の電子書籍には問題がある。組版を表現するフォーマットの問題、耐え難いほどに低DPIの問題、DRM等々。

2010-09-02

誰が得するのか分からない配列の仕様

配列において、あるエンティティが、同じスコープ内で前方宣言されていて、要素数が指定されている場合、省略された要素数は、すでにある宣言から得られる。

extern int a[5] ; // int [5]型のエンティティaの宣言
int a[] ; // OK、型はint [5]となる

この仕様は、実に最新のN3126で規定されたものである。しかし、既存のコンパイラーが、このコードを問題なく通すところを見ると、おそらく、すでにこのような合意があり、それを明文化しただけのように思える。