C言語の関数ヘッダに、IN/OUT区分は必要か?
データの内容が変化するパターン
C言語の関数ヘッダに記述される、引数のIN/OUT区分は、関数の引数に渡されたデータの内容が書き換えられるか、それとも書き換えられないか、2つのうちのどちらになるかを表すものです。
しかし、引数に渡されたデータの扱われ方は、単純にこの2つに区別される訳ではありません。詳しく見ると、下記のように3通りのパターンに分類できます。
- 深い不変
- 浅い不変/深い更新
- 浅い更新
- 深い不変
-
引数に渡されたデータの内容が、まったく書き換えられない場合です。
例えば、次のような名簿データを、関数に渡したとしましょう。
深い不変性がある時、関数を呼び出しても、このデータはまったく変化しません。
キーワード欄に入力された文字列を含む諺がデータベースから取り出され、そのうちの最初の3個が画面に表示される。
- 浅い不変/深い更新
-
引数に渡されたデータが直接持っている値は書き換えられませんが、そこから参照されているデータの内容が書き換えられる場合です。
名簿データの例では、名簿に載っている会員の、個人情報の一部を書き換えるような場合が、浅い不変/深い更新に相当します。
- 浅い更新
-
引数に渡されたデータが直接持っている値が書き換えられる場合です。
名簿データの例では、名簿に何人かのデータを追加したり、何人かのデータを削除したりする場合が、浅い更新に相当します。
引数の不変性/更新性の3つのパターンについて、ソースコードで確かめてみましょう。前述の、名簿データの例を実装したものです。
typedef struct { char name[100]; int age; } Person; typedef struct { int size; Person* persons; } PersonList; /* 深い不変 */ static void print ( const PersonList* list ) { printf("%d人--------\n", list->size); for (int i = 0 ; i < list->size ; i++) printf("%s (%d)\n", list->persons[i].name, list->persons[i].age); } /* 浅い不変/深い更新 */ static void modify ( const PersonList* list ) { list->persons[0].age = 33; } /* 浅い更新 */ static void remove ( PersonList* list ) { list->size = 1; } void main ( ) { PersonList list; list.size = 2; list.persons = new Person[2]; strcpy(list.persons[0].name, "鈴木"); list.persons[0].age = 30; strcpy(list.persons[1].name, "佐藤"); list.persons[1].age = 25; print(&list); modify(&list); print(&list); remove(&list); print(&list); }
このソースコードを実行すると、次のように出力されます。
2人-------- 鈴木 (30) 佐藤 (25) 2人-------- 鈴木 (33) 佐藤 (25) 1人-------- 鈴木 (33)
関数ヘッダに記述される、引数のIN/OUT区分では、深い不変性がある場合を[IN]とし、更新が行われる場合は、浅い更新と深い更新を区別せず、どちらも[OUT]とすることが多いでしょう。
しかし、[IN]と[OUT]の違いが明確でないと、深い更新が行われるにも関わらず、IN/OUT区分に[IN]と記述してしまい、統一がなくなってしまうこともあるので、注意が必要です。
const修飾子の意味
C言語でもC++言語でも、関数の引数にポインタを渡す時、内容を書き換えない場合は、const修飾子を付けるべきとされています。そのため、きちんとconst修飾子を付ければ、関数ヘッダにIN/OUT区分を書く必要はないようにも思われます。
しかし、const修飾子を付けた時の不変性と、関数ヘッダで[IN]と書いた時の不変性には、大きな違いがあります。
関数の引数にconst修飾子を付けた時は、浅い不変性は守られますが、深い不変性は守られません。即ち、浅い更新は不可能でも、深い更新は可能なのです。
前述の、名簿データのソースコードの例でも、浅い不変/深い更新を表すmodify関数の引数には、const修飾子が付けられていますが、呼び出すことによって、名簿の内容は変化しています。
関数の引数に付けられるconst修飾子は、データの内容が書き換えられない、ということを意味してはいません。関数の利用者に、データの内容がまったく書き換えられない、ということを伝えるには、関数ヘッダで[IN]と記述するしかないのです。
余談ですが、関数の引数にconst修飾子を付けて宣言しても、コンパイラの警告を無視すれば、浅い更新を行うように実装することもできてしまいます。
IN/OUT区分も、const修飾子も、どちらも必要だ
引数に渡されたデータの内容が変化する3つのパターンと、IN/OUT区分、const修飾子の関係は、下記の通りです。
不変性 | 更新性 | IN/OUT区分 | const修飾子 |
---|---|---|---|
深い不変 | [IN] | ○ | |
浅い不変 | 深い更新 | [OUT] | ○ |
浅い更新 | [OUT] |
関数ヘッダのIN/OUT区分にしても、const修飾子にしても、いずれにせよ、それだけでは、引数に渡されたデータの内容がどのように変化するかを、厳密には表すことはできません。この両者を組み合わせて記述する必要があります。
C++言語でも、IN/OUT区分は有用か?
C言語と同じ関数ヘッダでは役に立たない
現在では、C言語よりも、オブジェクト指向言語であるC++言語が主流となっています。
C++言語を使う場合でも、メソッドの説明として、従来どおりの関数ヘッダを書くことも多いです。しかし、C言語の関数と、C++言語のメソッドでは、その概念は大きく違っています。
C言語では、1つ1つの関数呼び出しは、互いに独立しています。例えば、次のコードを見てください。
Foo foo; func1(&foo); func2(1);
ここでは、関数func1の引数に、fooというデータを渡しています。関数func1に渡したデータは、関数func1を呼び出している間しか、使用されません。関数func2を呼び出した時に、fooというデータの内容が書き換えられることはありません。
ところが、C++言語のメソッドでは、事情が異なります。オブジェクト指向言語のメソッドは、オブジェクトに対して呼び出します。時には、引数に渡したデータを、呼び出し先のオブジェクトが保持して、まったく別のタイミングで、内容を書き換えてしまうことがあるのです。
例えば、次のコードを見てください。
Bar bar; Foo foo; bar.func1(&foo); bar.func2(1);
C言語の場合と違って、C++言語の場合は、メソッドfunc2を呼び出した時に、メソッドfunc1に渡したfooというデータの内容が、書き換えられる可能性があります。
この時、クラスBarの実装は、例えば次のようになっています。
class Bar { private: Foo* foo; public: void func1 ( Foo* _foo ) { foo = _foo; } void func2 ( int n ) { foo->Set(n); } };
メソッドfunc1の呼び出しでは、引数に渡したfooの内容は書き換えられません。そこで、従来どおりの関数ヘッダでは、引数_fooは[IN]と説明されます。メソッドfunc2の引数nも同様です。
関数ヘッダには[IN]しか記述されていないのにも関わらず、実行すると、引数に渡されたデータの内容が書き換えられます。これでは、関数ヘッダが、利用者に誤解を与える結果になってしまいます。
C++言語では、クラスヘッダにIN/OUT区分を書くべきだ
C言語とC++言語における、引数に渡されたデータの扱われ方の違いは、次のようにまとめられます。
- 関数単位の不変性/更新性(C言語の場合)
-
C言語では、呼び出した関数の中でしか、引数の内容は書き換えられません。そのため、関数の引数に渡されたデータの内容が、その関数を呼び出した時に書き換えられるかどうか、が焦点となります。
- クラス単位の不変性/更新性(C++言語の場合)
-
C++言語では、同一のオブジェクトを使い続けている限り、メソッドの呼び出しを超えて、引数の内容が書き換えられる可能性があります。
あるオブジェクトに対して、あるメソッドを呼び出した時に、あるデータを引数に渡したとします。C++言語の場合は、そのオブジェクトに対して、将来、何らかの操作を行った時に、さきほど渡したデータの内容が書き換えられるかどうか、が焦点となります。
これを踏まえると、C++言語においては、クラス単位で、IN/OUT区分を説明する必要があることが分かります。即ち、1つ1つのメソッドに関数ヘッダを書くのではなく、クラス全体のクラスヘッダに、すべてのメソッドの引数のIN/OUT区分を、まとめて記述する、ということになります。
前述のクラスBarでは、次のようになります。
/* * Name: Bar * * Description: Example. * * Arguments: * Name Type IN/OUT Description * _foo Foo* [OUT] Foo instance. * n int [IN] Set to foo instance. */
今の作りを説明するよりも、あるべき姿を定義する
オブジェクト指向言語には、インターフェースという概念があります。インターフェースでは、メソッドの引数や返値と、そのメソッドが果たすべき役割だけが定義されており、具体的な処理は実装されていません。
C++言語では、インターフェースは抽象クラスとして実装されます。また、実装を持たないメソッドは、純粋仮想関数と呼ばれます。
インターフェースでは、メソッドの引数に渡されたデータの内容が書き換わるかどうかは、実装クラスに依存します。では、インターフェースのクラスヘッダでは、IN/OUT区分はどうすれば良いのでしょうか?
引数に渡されたデータの内容が書き換えられるかどうかは、メソッドが果たすべき役割の一種と考えられます。つまり、インターフェースを定義した時点で、引数に渡されたデータの内容が書き換えられるかどうかも、同時に定義するべきです。
インターフェースのクラスヘッダには、引数が書き換えられるべきかどうか、を書くことになります。実際にそのインターフェースの実装クラスを作成する時は、インターフェースで決められた通りに作ります。もし、インターフェースのクラスヘッダで[IN]と書かれているのならば、実装クラスで勝手に値を書き換えるようなことは、行ってはいけません。
メソッドのオーバーライドについても同様です。仮に、クラスを作った時に、引数に渡されたデータの内容を書き換えていなかったとしても、将来、このクラスを継承した子クラスでメソッドをオーバーライドした時に、必要ならデータの内容を書き換えても良いのであれば、クラスヘッダには[OUT]と記述しておくべきです。
IN/OUT区分を、ソースコードで表現するには?
IN/OUT区分は、利用者と開発者が取り交わす契約である
引数に渡されたデータの内容が書き換えられるかどうかは、メソッドを呼び出す利用者と、呼び出されるオブジェクトの間で決められる、約束事の1つです。このように、クラスを利用する上での約束事を先に取り決めて、それに基づいた設計・実装を行う考え方を、『契約による設計』(Design by Contract)と言います。
クラスヘッダに[IN]と書くことは、クラスの開発者が、利用者との間で、「このデータの内容は決して書き換えない」という契約を結んだ、という意味になります。とはいえ、コメントとして書いておくだけでは、必ずしも契約が正しく守られる、という保証ができません。たとえ、クラスヘッダに[IN]と書かれていたとしても、開発者がそれを見落としていれば、データの内容を書き換えてしまい、思わぬ不具合を生じる可能性があります。
データの内容が書き換えられるかどうかを、コメントだけでなく、ソースコードで表現できるのが理想的です。例えば、const修飾子を使っていれば、誤って浅い更新をしようとしても、コンパイラがエラーを表示してくれます。しかし、前述した通り、const修飾子では、深い不変性が守られることまでは、保証できません。
契約による設計は、アスペクト指向プログラミングで実現する
契約による設計を実現する方法としては、アスペクト指向プログラミング(AOP)が知られています。
アスペクト指向プログラミングとは、それぞれのクラスが行う本質的な処理(本質的な関心)とは別に、何らかの機能が必要な時に、その機能をクラス本体とは分けて記述する(関心事の分離)、という手法です。分離される機能は、さまざまなクラスで必要となる共通的なものであることが多いです(横断的な関心)。
クラスの利用者と開発者が取り交わした契約が、きちんと守られているかどうかは、すべてのクラスでチェックする必要があります。データを表すクラスでも、ファイルの入出力をするクラスでも、複雑な計算を行うクラスでも、どんなクラスでも必要な機能となります。そのため、契約による設計は、アスペクト指向プログラミングとして実現するのが適しています。
下記のページでは、アスペクト指向プログラミングによって、契約による設計を実現する例が紹介されています。
アスペクト指向プログラミングで実現した時の問題点
しかし、実際の業務にアスペクト指向プログラミングを導入するには、下記のような大きな問題があります。
- C++言語の言語レベルではサポートされていない。
- アスペクト/契約の定義方法が、フレームワーク依存であり、標準的な文法が存在しない。
- C++言語では、アスペクト指向プログラミングを実現する実用的かつ標準的なフレームワークがない。
C++言語やJava言語は、オブジェクト指向の概念を、言語レベルでサポートしています。しかし、アスペクト指向は新しい概念なので、これを言語レベルでサポートしている実用的なプログラミング言語は存在しません。もし、この概念に基づいた新しいプログラミング言語が誕生しても、それがC++言語並みに普及して、業務で使うのに問題がない状況になるには、相当の時間がかかると思われます。
また、契約による設計を、アスペクト指向プログラミングによって実現すると、契約違反を実行時にしか検出できない、という問題もあります。例えば、const修飾子を使っていれば、誤って浅い更新をしようとしても、コンパイラがエラーを表示してくれます。同様に、深い不変性を守る、という契約を定義した時に、誤ってデータの内容を書き換える処理を書いてしまった時は、コンパイル時にエラーが表示されることが理想的です。
アスペクト指向プログラミングによって実現される契約による設計では、契約の内容は、かなり自由に定義できます。逆に言えば、契約の内容を自由に定義できるために、仕組みを言語レベルでサポートしたり、標準的な文法を取り決めたりすることが、たいへん難しくなっています。また、契約違反を実行時にしか検出できない原因にもなっています。
IN/OUT区分に関してのみ、プログラミング言語を拡張する
IN/OUT区分に関して、プログラミング言語を少しだけ拡張する、という方法も考えられます。
具体的には、不変性/更新性を記述するためのマクロを定義し、それが守られているかどうかをチェックするプリプロセッサを作成することが考えられます。この方法では、不変性/更新性についての契約が守られていないことを、コンパイル時に検出することも可能となります。
デザインパターンで解決する
例えば、あるクラスのオブジェクトが必ず1つだけしか生成されない、という制約は、Singletonパターンを使うことによって、オブジェクト指向設計の枠組みの中で解決できます。同様に、引数で渡されたデータの内容が書き換えられない、という制約も、デザインパターンで解決できないでしょうか?
引数に渡されるデータについて、深い不変性が守られる場合は、不変オブジェクト(immutable object)を表すクラスを作り、それを引数に取るようにメソッドを定義する、という方法が採用できるかもしれません。但し、この点については、まだ深く考えていません。