ネタ元:[C] びっくりー
えっと、Cのお話のようですが、C++ な人としてはちょっと変くね?というところかなw
time_t now = time( &now );
がどう解釈されるのか?ですが、「言語仕様?代入演算子ですよ。」では、おそらく飲み下せないから、ネタになってるのかな?とw
ってことで独自に構文解釈を試みてみましょう。
まずは、揺らぎの原因である=演算子の存在から。
=演算子は、代入演算子でしょ。ってのは、C系の言語を使ってる人なら皆知ってるとは思いますが、もう一つの特性として
「変数定義の式中にある=演算子は必要に応じて初期化演算子(コピーコンストラクタ呼び出しのシンタックスシュガー)として扱う」
というものがあります(ここで一番問題なのは、必要があればの部分なわけですが...w)。
ちなみに、これ自体は仕様書にこう書かれているではなく、あまたの書籍で上記のように書かれているということです。こっちのほうが実際の現場じゃ重要なのでw
ま、細かいことはさておき、それぞれの演算子解釈をとりうるという仮定の下、両方のパターンで構文解釈を試みてみましょう。
まずは、初期化演算子です。初期化演算子は先にも書いたとおり、コピーコンストラクタ呼び出しのシンタックスシュガーですから
time_t now( time( &now ) );
という形に解釈されることになります。
この解釈でコード生成するとした場合、処理の段取りとしては
- &now で、now へのポインタを得る。
- これを引数として time 関数を呼び出す。
- now のために time_t 型のエリアを用意する。
- 2の戻り値を引数とした time_t のコピーコンストラクタを呼び出し、変数now を初期化する。
という流れになり、4が終わった時点で、ようやく now が変数として利用可能となります。
したがって、1の部分ではまだ、now は存在しないという解釈となり、結果としてコンパイルできないといえるでしょう。
#実際に、C++ で上記のように書くと now が未定義というエラーになります
次は、代入演算子としてみた場合です。代入演算子の場合、最初の式は、
time_t now; now = time( &now );
という式に解釈されます。なぜ、そうなるのでしょうか?
代入演算子は
- 右辺の式を処理する
- 左辺の式を処理する
- 右辺の式の結果を左辺の式の結果に代入する
と覚えていると思います。まぁおおむね間違いではありませんが、これはあくまでも概念レベルといっていいでしょう。
CやC++を使う場合、もう一つ大事なものとして、いついかなる場合でも型を意識するというのを忘れてはいけません。
上記解釈にはこの型を意識するという部分が欠落していますので、それを追加してみましょう。
代入するためには、何といっても受け入れるための型が必要となります。そのため、最初に左辺がとりうる型というのを確定しなければなりません。
それを追加して改めて代入演算子の処理を解釈しなおすと
- 左辺の内容が実在することを確認する(time_t now を切り出し、now があることを確定する、結果nowが定義される)
- 左辺がとりうる型をその式より解釈する(now の型を確認する)
- 右辺の処理の結果(time_t time( time_t* )の呼び出し)から、得られる型を確定する
- 右辺の結果を一時的に格納する変数を実行時に用意する(見えない変数)
- 右辺の式を処理し、その結果を(戻り値)を4に代入する
- 左辺の式を処理し、その結果に、4の変数から代入する
という流れになります。
ここで大きなポイントを占めるのは、最初の1の部分です。左辺の処理結果で必要とされる型の解釈を行うという部分では、now を持ち出してくるわけですが、その時点でnowは、確定しなければならないことになり、結果として now が定義されてしまうということです。
そのため、実際に右辺を処理する段階では、すでに now は定義された後になり、結果的にはコンパイルに支障がないということになります。
ま、厳密にいえば言語仕様としてはもうちょっと違うと思いますけど、一般的な解釈ではこれで十分です(他のC系言語では若干異なる可能性はありますけどねw)。
さて、ここまではおおむね問題はないでしょう。飲み下すためにはなぜ初期化演算子にならないのか?という部分をもう少し細かく見ていく必要があります。
そのためには、型の定義を見てみる必要があります。
time_t 型は MSC14.0(=VC++8=VS2005)のデフォルトの設定では、__time64_t の typedef です。__time64_t は __int64 のtypedef です。
__int64 は、MSCの独自定義型ですが、int の 64bit 版といってもいい方であり、実質的に埋め込み型として用意されているものです。
そのため、time_t 型はコンパイル時点では埋め込み型の__int64 として扱われることになります。
__int64 は、埋め込み型なので、当然ですが、コピーコンストラクタもデフォルトコンストラクタもありません。これは、C との互換性により残された部分ですので、C++ であっても、C であっても同じ扱いとなります。
したがって、最初の構文は初期化演算子として扱われることはない。という結論になります。
ちなみに、C++では、time_t now( time( NULL ) ); という構文の書き方を許しています。これは実際には上記代入処理と同じことを行うのですが、これもまたシンタックスシュガーとして言語仕様で規定されたものです。
最近ではめったに見かけなくなりましたが、
int n( func() );
というような定義が可能になっているということですね。
ちなみに、初期化演算子の解釈で表記した、time_t now( time( &now ) ); は、表記は可能ですが、コンパイルエラーになります。
これが、time_t now( time( NULL ) ); ならエラーにはならないんですけどね。
この辺りは、C++を難しいと感じる部分の一つかも知れません。ちょっとはまってる人を見かけた場合は、それシンタックスシュガーだよと教えてあげるといいでしょう。
おまけ
えー、time 関数で、引数に渡す変数と、戻り値を受ける変数を同じにした場合の処理ですが...はっきり言って無駄ですw
この場合、どちらかを省略するのが普通です。なので、
「無駄な代入があって余計なコードが生成されるので、time( &now ) ではなく、time( NULL ) とするべきである。」
とレビューで突っ込んでください。
どのくらい無駄があるかは、実際にリリース版でコンパイルしてみればわかりますが、少なく見積もっても2サイクルは、最新CPUでも無駄になっているはずです。
ま、いまどきのCPUで1サイクル増えた減ったって大したことじゃないでしょうけど...
ちりも積もれば山となるという言葉もありますからねw