2010-01-11

C++0x本:懸念事項

いま抱いているC++0x本の構想には、恐れがある。それは、純粋に言語を解説しようとした挙句、あまりにもマニアックすぎて、極少数の人にしか読まれないのではないかという懸念だ。すでに刊行されている他のパーフェクトシリーズは、私の構想ほど、言語仕様を厳密に解説してはいない。C#やJavaならそれでいいのかもしれないが、C++の場合は、厳密に解説しなければ、本当の意味で理解することは出来ない。

C++は、実に複雑で難しい文法となっている。その理由は、Cの上に付け加えたからで、このcontext-sensitiveな文法の責任は半分以上、Cにある。

たとえば、宣言文である。宣言文は非常に難しい。よく、ポインタが分からないと言われているが、その理由の半分は、宣言文にある。

なぜ、宣言文が難しいのかというと、const、volatileや、ポインタは、前置するが、配列や関数などは、後置するからだ。

例えば、以下のコードで、typeの型は何なのかを考えてもらいたい。

// int
typedef int type ;

// intへのポインタ
typedef int * type ;

// intの配列
typedef int type[10] ;

// intの配列へのポインタ?
// intへのポインタの配列?
// 正解は、intへのポインタの配列
typedef int * type[10] ;

// intの配列へのポインタ
typedef int (*type)[10] ;

// ハァ?
typedef int (*(*type)(int (*)(int [])))(int []) ;

このように、前置と後置が組み合わさっているために、非常にわかりづらい文法になっている。ちなみに、最後の例を、人力で解釈しようとしてはならない。これは、int ( int [] )という関数へのポインタを引数に取り、int ( int [] )という関数へのポインタを戻り値として返す関数へのポインタである。

もちろん私は、最後の例を脳内で構築したわけではない。チートを使った。

typedef int func_type( int [] ) ;
typedef func_type * func_type2( func_type * ) ;
typedef func_type2 * type ;

std::cout << typeid(type).name() << std::endl ;

typedef相当のものを持つ言語というのは、C/C++以外には、あまり見られない。C/C++では、typedefは絶対に必要な機能である。あるいは、他の言語では、別に必要になるほど、宣言文がややこしくなることがないのだろうか。

たとえば文字列リテラルである。文字列リテラルの方は、char const []である。

// char const [13]
// (null文字を含む)
auto type = "hello world!" ;

配列は、ポインタに変換できる。これ自体は、邪悪だが、仕方がない機能である。

// 配列はポインタに変換できる
char const * ptr = "hello world!" ;

// あるいは、クソ真面目に書くと
char const * ptr = &"hello world!"[0] ;

わざわざ文字列程度に、添字とアンパサンドを書きたくはない。したがって、配列から先頭要素へのポインタに変換できる文法は、邪悪だが、仕方がない。ところが、const性まで消し去ってしまうのはいかがなものか。

// いいの?
char * ptr = "hello world!" ;

// これらは間違い
// じゃあなんで暗黙のうちにconstを消しされるのさ?
ptr[0] = 'x' ;
"hello world"[0] = 'x' ;

これは、歴史的な理由である。そもそもCには、constなどというものはなかった。現在のCにはあるが、これはC++から借りてきたのである。Bjarne Stroustrupは、Cのこのような側面を嫌っていた。

Cはこの他にも、かなりの危険な型変換を、明示的なキャストなしに、暗黙のうちに行うことができる。非常に邪悪な言語である。こんな邪悪な言語を土台にしたら、C++が邪悪になるのは当然の話だ。

じゃあ、なんでCなんか土台に選んだのだという批判は、場違いである。Cを土台にしたからこそ、組み込みからデスクトップまで、C++が広く使われているのだ。現代からみて、過去を批判するのは簡単だ。なぜ最初から、文字列にはUnicodeと同等の規格を制定して使わなかったのか。文字が7bitで足りるわけがない。なぜ最初から、CPUのアドレスを64bitにしておかなかったのか。32bitアドレッシングで足りるわけがない。

この手の論者は、数十年後に、自分が批判されるだけである。「なぜ、2010年の時点で、CPUのアドレスを128bitにしておかなかったのか。64bitアドレッシングで足りるわけがない」

C++全部を詳細に解説する本というのは、今までに見たことがないし、洋書でも知らない。C++ Templatesは、Template専門だが、かなり詳しく解説している。最も私の理想とする本に近い。

C++ Templatesのように書くとするならば、まず「どのように」書くかという基本編と、「なぜ」こう書かなければならないのかという、詳細編に分けることができる。その方が分かりやすいだろうか。

5 comments:

I.S. said...

配列はpointerに暗黙的に変換できますが、構造体はそうはできません。これは納得いかないなあなどと思っていましたが、仕方のないことだと思ってます。何でchar型配列だけliteralがあるの? 何で関数は関数pointerに変換できるの?など、他にも色々ありますね。

ですが、そのどれもがただ「ある目的のためには便利だから」、これに尽きますね。

宣言の複雑さですが、これは宣言なのだから実際の使用と別の書きかたでもいいはずなのに、Cでは実際の書きかたと似せるようにしたため、初心者が却って混乱する原因になってますね。

int *p;
ptr < int > p; // C++的な書きかたならこうなるか

int a [ 10 ];
array < int, 10 > a; // C++ではこう

int f ( int );
function < int, int > f; // C++的にはこう

int *ap [ 10 ];
array < ptr < int >, 10 > ap; // C++的にはこう

int ( *pa ) [ 10 ];
ptr < array < int, 10 > > pa; // C++的にはこう


「どのように」と「なぜ」を分けるのはいいかもしれませんね。初心者は「どのように」のほうにより興味があるでしょうから、分離することで、読みやすくなると思います。

江添亮 said...

結局、Cは邪悪なんですよね。
メンバ関数へのポインタは、アンパサンドをつけないといけないようになってますけどね。

萌ゑ said...

Cはねえ・・・高級マクロアセンブラ的な使い方をされていますから、
使いやすさを最優先して論理的な一致を無視している部分がいくつかあり
ますね

吐くコードもポインタさえ使わなければ結構綺麗です
ポインタを使うと例のエイリアスの問題が出て来てFortran信者にいつま
でも笑われる原因になっています

そこでC99ではrestrictポインタを採用したのですが、あまりにも危険だ
という事でC++では採用されませんでした

とある処理系では独自に__restrictとか予約語を拡張してますけどね

SubaruG said...

本文の趣旨とは少し外れますが、

// char const [13]
// (null文字を含む)
auto type = "hello world!" ;

における変数 type の型は、 auto によって変数に束縛されているので、 decay されて char const* になる、ということを、念のため記しておいてもいい気がします。

江添亮 said...

その通りです。
去年の一月の時点では、auto指定子の挙動を詳細に調べていなかったのです。

auto指定子の型は、argument deductionのルールを使って決定されるので、関数の仮引数と同じ型変換が適用されます。
したがって、この場合、配列からポインターへの型変換が行われます。