前回はC関数が受け取る値(引数)を説明しました。今回は、C関数が返す値を取り上げます。
C関数というのは、呼び出し元から要求された処理を行い、その処理結果を要求元に返すソフトウェア部品です。関数を使用すれば、プログラムの内容が分かりやすくなります。この意味では、C関数は、後の回で登場する「クラス」ほどではないにしても、Cプログラムを抽象化(分かりやすく)するための表現上の道具です。
私たちはこれまで、次のようなプログラムの機能と表現上の意味を詳しく調べてきました。
#include <stdio.h>
int main()
{
const char* ch = "I don't know anything about programming!\n";
printf (ch);
}
このプログラムは、mainとprintfの2種類の関数で構成されています。今回は、「int main()」関数の「int」の意味を詳しく考えてみることにします。
上のサンプルコードを見ると、main関数はint型の値を返すといっておきながら、何も返していません(returnというコードが記述されていない)。ここでは結論だけを示しておきますが、(筆者のVC++2003.NET)コンパイラは次のようなコードを背後で作成し、0という値をこっそり返しています。
00401038 33 C0 xor eax,eax
このコード内に見える「XOR」というコードは、「排他的論理和」という演算処理を高速に行うアセンブリコードです(この場合には、eaxレジスタの内容を0に設定しています)。それではここで、上のサンプルプログラムを次のように書き換えてみます。
#include <stdio.h>
int main()
{
const char* ch = "I don't know anything about programming!\n";
printf (ch);
return 0;
}
ご覧のように、「return 0;」というソースコードが追加されています。筆者のコンパイラは、この「return 0;」コードから、先ほどと同じ排他的論理和演算を行うアセンブリコードを作成してくれています。
今度は、次のサンプルコードを見てみましょう。
#include <stdio.h>
int main()
{
int retVal;
const char* ch = "I don't know anything about programming!\n";
printf (ch);
retVal = 0; // 返す値を丁寧に用意している
return retVal;
}
このサンプルプログラムは、main関数がint型の値を返す過程を懇切丁寧に記述しています。一見すると、たいへん分かりやすいため、模範としたいコードのような印象を受けます。しかし、実際には、筆者のVC++2003環境では、次のようなアセンブラコードが作成され、実行速度が落ちてしまいます。
0040103A C7 45 FC 00 00 00 00 mov dword ptr [retVal],0
00401041 8B 45 FC mov eax,dword ptr [retVal]
このアセンブラコードを完全に理解できる必要はありませんが、「mov」(移動せよ!)という命令が2箇所で使われ、実行速度が落ちていることを印象に残しておいてください。この場合も、「eax」レジスタに0が設定されています(つまり、呼び出し元に0を返していること)。
筆者のVC++2003コンパイラは、「return 0;」というコードが記述されていてもいなくとも、まったく同じアセンブラコードを作成していました。視点を変えると、"コンパイラは、自分なりに状況を判断し、排他的論理和演算を行うアセンブラコードを筆者の許可を受けずに、勝手に作成していた!"、といえます。
ここで問題ですが、コンパイラはどのような情報を基にして、"自分なりに状況を判断"していたのでしょうか?"。次のコードにヒントがあるようです。
int main()
このコードは、main関数はint型の値を返す、という意味を持っています。コンパイラは、int型の値を返すという、ソースコード内に記述されている宣言情報を頼りに状況を判断しているらしいのです。それでは、このあたりの事情を少し詳しく探ってみましょう。次のコードをご覧ください。
#include <stdio.h>
int main() ===> int型の値を返す、と宣言しているが...
{
float retVal; ===> int型ではなく、float型の値を返す準備
const char* ch = "I don't know anything about programming!\n";
printf (ch);
retVal = 0.00; ===> 返す値を設定
return retVal; ===> float型の値を返そうとしている。矛盾発生!
}
このコードを見ると分かるように、このmain関数はint型ではなく、float型の値を強引に返そうとしています。論理的な矛盾(専門的には、型の不一致)が発生しているのですが、筆者のVisual Studio.NET 2003では警告が出るだけで、無事にコンパイルすることができます。出来上がったEXEプログラムを実行すると、これまた不思議なことに、float型の値ではなく、0という整数値(int型の値)が返されます。このような事実を総合すると、コンパイラは、「int main()」のintを参考にして、0.00(float型の値)を0(整数型の値)に変換している、ということが(なんとなく)分かります(一種の「暗黙の型変換」)。今度は、ソースコードの一部を次のように変更してみます。
#include <stdio.h>
float main() ===> intをfloatに変更してみる
{
float retVal;
const char* ch = "I don't know anything about programming!\n";
printf (ch);
retVal = 0.00;
return retVal; ===> 不思議なことに、この値は返されない!
}
このソースコードをコンパイル・リンクし、出来上がったEXEプログラムを実行すると、たいへん不思議なことに、筆者の環境では、float型の値ではなく、「41」という整数値が返されます。お時間のあるときにご自分の環境でぜひ挑戦してみてください。もしかすると、コンパイラによっては、エラーや警告が出て、EXEプログラムは作成されないかもしれません。
それでは最後に面白い実験をしてみましょう。次のようなコードを用意してみました。
#include <stdio.h>
int main()
{
float retVal;
float getVal;
const char* ch = "I don't know anything about programming!\n";
printf (ch);
getVal = AddTest(2, 3);
printf ("%f\n", getVal); ===> 5ではなく、5.000000と表示される
retVal = 0.00;
return retVal; ===> float型からint型への変換は行われない
}
float AddTest (int a, int b) ===> float型の値を返すと宣言
{
int c;
c = 0;
c = a + b;
return c; ===> int型の値を返している。矛盾発生。
}
このプログラムを実行すると、2と3の和が「5」ではなく、「5.000000」と表示されてきます。理由はお分かりですね。(使用した
コンパイラは、「float AddTest (int a, int b)」の「float」という型情報を基に状況を判断し、整数値の「5」を浮動小数点「5.000000」に変換しているのです。より具体的に言えば、変数cの値を整数値(int)から浮動小数点値(float)に変換しているのです。この場合の変換は成功しますが、状況が複雑になると常に成功するとは限りません。実際、main関数内では、float型からint型への変換は行われていません。以上から、このような論理的な矛盾(開発上の勘違い)の発生は可能な限り避けるべきです。
Cプログラミング言語の学習を始めたばかりの皆さんは、関数は何らかの値を返す、という表現や説明をなんとなく理解されていたと思います。しかし今回触れたように、「何らかの値を返す」作業にはコンパイラというものが深く関与しています。コンパイラはきわめて単純な情報を基に、その都度、(勝手な)状況判断を行っています。状況を判断することは、私たち人間にとってもたいへん難しいものです。コンパイラを全面的に信用するのではなく、処理する情報の種類(「型」)をきちんと理解した(つまり、解決すべき問題をきちんと分析・整理した)上で、ソースコードを書くべきです。上のコードは、次のように記述すべきでしょう。
#include <stdio.h>
int AddTest(int, int);
int main()
{
int getVal; ===> 必要なのは整数の和
const char* ch = "I don't know anything about programming!\n";
printf (ch);
getVal = AddTest(2, 3);
printf ("%d\n", getVal);
return 0;
}
int AddTest (int a, int b) ===> 要求されているのは整数の和
{
int c;
c = 0;
c = a + b;
return c; ===> 要求されている整数型の値を返す
}
このコードを見ると分かるように、関数の意味、機能、および、使用目的をはっきりさせた上でプログラミング作業を開始しています。自分の考えをきちんと整理(勘違いを排除)し、その内容をソースコードとして表現(実装)することが基本です。
一口メモ
プログラミング言語C++設計者であるBjarne Stroustrup氏は、2005年1月一般公開されたこの論文の中で、"標準化委員会に当時参加していた委員のほとんどはコンパイラライタであり、アプリケーション開発者は一人しかいなかった"、と述べています。
コンパイラライタはコンパイラを書く人ですから、開発するプログラムの性質は大いに異なりますが、設計仕様書を実装する点では、平均的なアプリケーションプログラマと同じ立場にいる人たちです。しかし、この論文を読む限り、コンパイラライタは「C++仕様」作成に参加し、意見を述べています。"そのように設計されると、私たちの実装作業が難しくなります!"などと、仕様書の作成過程を監視し、反対意見を述べているわけです。かなり緊迫した場面です。
"そのように設計されると、実装が難しい"。これは、コンパイラライタの意見です。ということは、仕様書内に意味不明な規定(玉虫色の妥協)があると、"コンパイラライタはそれを自分勝手に解釈し、好き勝手に実装してしてしまう"、ということになります。このため、結果的に、使用するコンパイラに応じて異なる実行プログラムが出来上がることになります(面白い関連記事)。
Microsoftが公開するこの論文には、次のような文が記述されています。なお、この論文は2005年4月10日DOCファイルとして一般公開されましたが、20日経過後、MSPXファイルとして再構成された上で、一般公開されています。一部内容も更新されていますから、"重要資料"の一つと考えてよいでしょう。
These problems are due more to the C++ implementation and the kernel environment than to the inherent properties of the C++ language.
この文を簡単に解釈してしまえば、"問題はプログラミング言語(C++設計仕様)にあるのではなく、C++実装(コンパイラライタの解釈)にある"ということです。興味のある方は、上記論文を自分の目で確認してみるとよいでしょう。ここでは、コンパイラライタの判断が常に正しいとは限らない、ことをきちんと覚えておきましょう。
ソースコードを書くときには、"分かりやすく書く"や"単純に書く"ことなどを心がけるとよいと思います。完璧なコンパイラなどこの世にないのです。オブジェクト指向プログラミングに興味のある方は、ぜひ上に紹介したMicrosoft公開論文を精読するとよいでしょう。複雑で立派なクラスとクラス階層を設計したとしましょう。残念ながら、そのソースコードがコンパイルされる保証はどこにもないのが現状です(参考例_1、参考例_1)。時間をかけて作り上げた、"役に立たない"ソースコードの作成者は、経営者から見ると、「先見性がなく、かつ、技術力もない、それゆえ経営を圧迫する、好ましからざる」従業員となります。大きな流れを読むことが何よりも大切です。
人間や人間の価値、あるいは、人間社会を一様に定義することは誰にもできません。C++設計思想はこのような人間と社会の奥行きの深さをしっかり把握しています。C++の歴史と哲学を知ることは、コンパイラとその性能の歴史を知ることです。C++の歴史と哲学を知ることは、最新統合開発環境(IDE)や現代ソフトウェアの傾向と今後を評価するための基礎知識を提供してくれます。