C言語の最新事情を知る(4)
C99でリソース管理ライブラリを作ってみる
前回まではC99/C11の仕様について見てきた。今回は、従来のCプログラミングが、C99の仕様を活用することで、どのように変化するのかを取り上げる。
これまで、C99とC11の仕様について主だったところを紹介してきた。今回は応用編として、C99の仕様を用いることで日々のプログラミングがどのくらい簡素化、あるいは分かりやすくできるか、その可能性を探ってみたい。
リソース管理ライブラリを作る
Cではリソース管理はプログラマーに委ねられている。fopen関数でオープンしたファイルは確実にfclose関数で閉じなければならないし、malloc関数で確保したメモリはfree関数で確実に解放しなければならない。これは時としてリソースリーク(=リソースの解放し忘れ)という面倒なバグを引き起こす。リスト1は典型的なファイルにテキストを書き込むコードである。
const char * const fileName = "hello.txt";
const char * const mode = "w";
FILE *fp = fopen(fileName, mode);
if (! fp) {
fprintf(stderr, "Cannot open file '%s' in mode '%s': %s\n",
fileName, mode, strerror(errno));
}
else {
if (fputs("Hello, world\n", fp) == EOF) {
fprintf(stderr, "Cannot write to file '%s' in mode '%s'\n", fileName, mode);
}
else {
printf("Wrote file.\n");
}
if (fclose(fp) == EOF) {
fprintf(stderr, "Cannot close file '%s' in mode '%s': %s\n",
fileName, mode, strerror(errno));
}
}
|
このコードにはいくつかの考慮点がある。
エラー処理
一般にfopen関数のエラー処理を忘れることは、あまりない。エラーが発生するとfopen関数の戻り値がNULLになるため、後続のアクセス関数もエラーになりテストで容易に発見できるためである。しかし後続のfputsといったアクセス関数の呼び出しについては、エラー処理が書かれていないコードを見かけたことが読者の皆さんにもあるのではないだろうか。さらにはfclose関数のエラー処理となるとかなり怪しいのではないだろうか。
一般にはfputs関数で少量のデータを書き出しても、それはバッファリングされ、実際のディスクへの書き込みは即座には開始されないことが多い。このため実際に書き込みが起きるのはfclose関数を呼び出したときになるので、ここでエラー処理を行うことは重要である。
例えばfopen関数の直後にgetchar関数を置いてキー入力待ちになるようにしておいてから、USBメモリを書き出し先としてプログラムを実行し、キー入力待ちの段階でUSBメモリを引き抜いてから、キーを叩いて後続処理をしてみてほしい。以下のように、fputs関数ではなく、fclose関数の呼び出し時点でエラーとなる処理系が多いと思われる。
Cannot close file '/Volumes/UNTITLED 1/hello.txt' in mode 'w': Input/output error
|
エラーの際のクローズ処理
よくある間違いとして、ファイルアクセスを行う関数の実装時に、fputs関数などのアクセスエラーを検出したとき、fclose関数を呼び出すのを忘れてしまうというケースがある。この場合、fopen関数が成功しているのでファイルはオープンされたままになっている。コーディング段階では、ファイルハンドルに余裕があるのでこの間違いには気付きにくい。しかし、この関数が定期的に呼び出されるようなものだと、ファイルアクセスエラーが解消されない限り、この関数は「どんどんとファイルハンドルを浪費して最終的にシステム全体が動作しなくなる」という障害を引き起こすことがある。前述のリスト1ではfputs関数でエラーが起きたときにも、fclose関数が呼ばれるようになっていることが分かる。
エラー原因の表示
リスト1では、エラーが起きたときにファイル名やモード、errno変数が示すエラーを表示しているが(さらには__FILE__マクロや__LINE__マクロを含めることが多い)、実際のコードではエラー時に十分な情報が書き出されていないことも少なくない。このようなケースでは、特に障害がまれにしか起きず、それも特定の環境でしか起きない場合、例え単純なバグでも障害の原因特定に膨大な時間を要してしまうことがある。
このようにリソース管理にはいろいろと考慮点が多く、個々の開発者にこうした配慮を求めるのは負担が大きい。今回はリソース管理をより簡単にするためのライブラリをC99の仕様を活用して作成してみたい。
リソース管理ライブラリの仕組み
リソース管理が厄介なのは、前節で見た通り、場面によって変化する処理と変化しない定型処理とが入り組んでいるためである(図1)。
ファイルアクセスの全体の流れや、オープン処理、クローズ処理といった部分は共通であるのに対し、エラー処理や実際にオープンされたファイルの処理は場面によってさまざまに変化する。今回は、定型処理はライブラリ側に任せつつ、変化する処理を関数ポインターで外出しにして変更を可能としてみよう。以下は今回作成するリソース管理ライブラリを使用し、「hello.txt」という名前のファイルに、文字列「"Hello\nWorld\n"」を書き込む例である(リスト2)。
#include <stdio.h>
#include <stdbool.h>
#include "file_accessor.h"
int main(int argc, char **argv) {
accessFile(&(FileAccessor) { // 1
.pName = "hello.txt",
.pMode = "w",
.process = writeAsText,
.pData = "Hello\nWorld\n",
});
}
|
通常であれば、リスト1に示したように、ファイルのオープン、クローズやエラー処理が書かれているはずが、リスト2ではaccessFile関数の呼び出しのみで完結していることが分かる。1の部分ではC99で追加された複合リテラルを用いている。ライブラリに渡すデータが多い場合、それらをそのまま関数の引数として渡すように設計すると、ずらりと大量の引数が並び、それぞれの引数が何を意味するのか分かりにくくなる。複合リテラルを用いるとメンバー名を指定できるため、それぞれの引数が何を意味するのか分かりやすくなる。もちろんメンバー名は省略しても構わないが、ここはあえて書くことで、それぞれの引数が何を意味しているのか明確にするのが狙いである。
ここでは複合リテラルを生成した後に、そのポインターを関数に渡しているが、ここで生成された複合リテラルのスコープはどこまで有効なのだろうか? 実際にaccessFile関数が実行されるときまで有効なのだろうか? 複合リテラルが自動記憶域で生成された場合、それは直近のブロック内で有効となることが仕様書に示されている。従って今回の場合はmain関数内で有効となるので問題無い。
次にFileAccessor構造体とwriteAsText関数の宣言を見てみよう(リスト3)。
#ifndef _FILE_ACCESSOR_H
#define _FILE_ACCESSOR_H
#include <stdio.h>
#include <stdbool.h>
typedef struct FileAccessor {
const char *pName;
const char *pMode;
void (*onOpenError)(struct FileAccessor *pThis, int errNo);
bool (*process)(struct FileAccessor *pThis, FILE *fp);
void (*onAccessError)(struct FileAccessor *pThis, int errNo);
void (*onCloseError)(struct FileAccessor *pThis, int errNo);
void *pData;
} FileAccessor;
extern void *accessFile(FileAccessor *pAccessor);
bool writeAsText(FileAccessor *pThis, FILE *fp);
#endif
|
FileAccessor構造体は今回使用する構造体で、以下のようなメンバーを持っている。
メンバー | 説明 |
---|---|
pName | アクセスするファイル名 |
pMode | アクセスするモード |
onOpenError | ファイルオープンに失敗したときに呼び出す処理 |
process | ファイルが開かれている間に実行する処理 |
onAccessError | processの処理で失敗したときに呼び出す処理 |
onCloseError | ファイルのクローズに失敗したときに実行する処理 |
pData | アプリケーションで自由に使用できるデータ |
writeAsText関数はFileAccessor構造体のprocessメンバーに指定できる関数で、構造体のpDataメンバーで指されたデータをテキストと見なしてファイルに書き込む関数である(FileAccessor構造体のprocessメンバーと同じ引数、戻り値型になっていることに注意してほしい)。
それでは最後にaccessFile関数と、writeAsText関数を見てみよう(リスト4)。
#include <stdio.h>
#include <errno.h>
#include <stdbool.h>
#include <string.h>
#include "file_accessor.h"
static void openError(FileAccessor *pThis, int errNo);
static void closeError(FileAccessor *pThis, int errNo);
static void accessError(FileAccessor *pThis, int errNo);
void *accessFile(FileAccessor *pAccessor) {
FILE *fp = fopen(pAccessor->pName, pAccessor->pMode); // 1
if (! fp) { // 2
(pAccessor->onOpenError ? pAccessor->onOpenError : openError)(pAccessor, errno);
return pAccessor->pData;
}
if (! pAccessor->process(pAccessor, fp)) // 4
(pAccessor->onAccessError ? pAccessor->onAccessError : accessError)(pAccessor, errno);
if (fclose(fp) == EOF) { // 6
(pAccessor->onCloseError ? pAccessor->onCloseError : closeError)(pAccessor, errno);
}
return pAccessor->pData; // 7
}
static void openError(FileAccessor *pThis, int errNo) { // 3
fprintf(stderr, "Cannot open file '%s' in mode '%s': %s\n",
pThis->pName, pThis->pMode, strerror(errNo));
}
static void closeError(FileAccessor *pThis, int errNo) { // 9
fprintf(stderr, "Cannot close file '%s' in mode '%s': %s\n",
pThis->pName, pThis->pMode, strerror(errNo));
}
static void accessError(FileAccessor *pThis, int errNo) { // 5
fprintf(stderr, "Cannot access file '%s' in mode '%s'\n", pThis->pName, pThis->pMode);
}
bool writeAsText(FileAccessor *pThis, FILE *fp) { // 8
return fputs((const char *)pThis->pData, fp) != EOF;
}
|
- 1accessFile関数は、まず指定されたファイル名を持つファイルを指定されたモードでオープンする。
- 2もしもファイルのオープンに失敗した場合、FileAccessor構造体のonOpenErrorメンバーが設定されていればその処理を、無ければデフォルトのエラー処理としてopenError関数を呼び出して呼び出し元に戻っていることが分かる。
- 3openError関数はデフォルトのオープンエラー処理で、単に標準エラー出力にメッセージを出力している。
- 4オープンに成功すれば、FileAccessorのprocessメンバーに指定された関数を呼び出す。process関数はbool型で成功/失敗を返すようになっており、失敗した場合は失敗処理を呼び出す。失敗処理はFileAccessor構造体のonAccessErrorメンバーが指定されていればその処理を、指定されていなければデフォルトのエラー処理としてaccessError関数を呼び出す。
- 5accessError関数はデフォルトのファイルアクセスエラー処理で、単に標準エラー出力にメッセージを出力している。
- 6最後にファイルのクローズ処理を行っている。ここでも同様に失敗したときには、FileAccessor構造体のonCloseErrorメンバーが指定されていれば、その処理を、指定されていなければデフォルトのエラー処理としてcloseError関数(9)を呼び出している。なお、4でファイルアクセスにエラーが起きた場合にもクローズは必要なので、4から直接呼び出し元に戻らずに、必ずクローズ処理を呼び出していることに注意してほしい。
- 7最後に呼び出し元にはFileAccessor構造体のpDataメンバーが返される。従って呼び出し元が用意する処理の中で、pDataメンバーに何らかのデータを設定しておけば、それをaccessFile関数の戻り値として受け取ることができる。
- 8writeAsText関数は、すでに解説した通り、FileAccessor構造体のprocessメンバーに指定できる関数の1つで、FileAccessor構造体のpDataメンバーで指されたデータを文字列と見なしてファイルに書き込む関数である。
- 9closeError関数は6で解説した通り、デフォルトのクローズエラー処理である。
このように定型処理の中に関数ポインターを用いて、アプリケーション固有の処理をはさみ込むようにし、それを複合リテラルで与えることによって、Cでもスクリプト言語のように柔軟で分かりやすい記述が可能になることが分かる。
accessFile関数の中に記述された動作はファイルに書くときだけでなく読み出すときにも利用できる。次はファイルからテキストを読み出す例を見てみよう(リスト5)。
……省略……
static ProcessLineResponse readLine(FileAccessor *pThis, const char *pLine) { // 2
fputs(pLine, stderr);
return LINE_PROCESS_CONTINUE;
}
int main(int argc, char **argv) {
……省略……
accessFile(&(FileAccessor) {
.pName = "hello.txt",
.pMode = "r",
.process = readAsTextLines,
.pData = &(LineProcessor) { // 1
.processLine = readLine,
}
});
}
|
- 1今回は改行で区切られたテキストファイルを読み込むことを想定している。ライブラリはファイルをテキストとして読み込みながら、読んだテキスト1行を、アプリケーションに渡す。これを実現するためアプリケーションから、行を処理するための関数をライブラリに渡せるように、LineProcessorという構造体へのポインターをpDataメンバーで受け取るようになっている。
- 2アプリケーションが用意した1行処理のための関数である。今回は単に受け取った1行をfgets関数で標準エラーに書き出してから、LINE_PROCESS_CONTINUEという値を返している。この戻り値をライブラリが受け取るとライブラリは次の行の読み出しに移る。結果として常にLINE_PROCESS_CONTINUE値を返すようにしておけば全行を処理できることになる。
それでは今回使用している構造体、定数の宣言を見てみよう(リスト6)。
……省略……
typedef enum { // 1
LINE_PROCESS_CONTINUE,
LINE_PROCESS_CANCEL,
LINE_PROCESS_ERROR,
} ProcessLineResponse;
typedef struct LineProcessor {
ProcessLineResponse (*processLine)(FileAccessor *pThis, const char *pLine); // 2
size_t lineBufferSize; // 3
char *pLineBuffer;
void *pData; // 4
} LineProcessor;
bool readAsTextLines(FileAccessor *pThis, FILE *fp);
|
- 1アプリケーションが用意する行処理関数が返す定数値である。リスト5の2で見た通り、LINE_PROCESS_CONTINUE値を返すとライブラリは次の行の読み込みに移る。LINE_PROCESS_CANCEL値を返した場合は、そこで行の読み込みを中断する。LINE_PROCESS_ERROR値の場合はエラーとして扱い行の読み込みを中断する。
- 2リスト5で見た通り、readAsTextLines関数を使用する場合には、行の処理を行うためのLineProcessor構造体のインスタンスをpDataメンバーに設定する。この構造体のprocessLineメンバーに1行を処理する関数へのポインターを設定する。
- 3ライブラリで1行を読み込むときのバッファーを指定する。リスト5では指定していないが、これについては後述する。
- 4すでにFileAccessor構造体のpDataメンバーは、LineProcessor構造体のインスタンスを渡すために使用してしまったので、アプリケーション固有のデータをやりとりできるように、ここにpDataメンバーを用意している(今回は使用していない)。
最後に、readAsTextLine関数の中身を見てみよう(リスト7)。
bool readAsTextLines(FileAccessor *pThis, FILE *fp) {
LineProcessor *proc = (LineProcessor *)pThis->pData; // 1
size_t bufSize;
char *pBuf;
if (proc->lineBufferSize) { // 2
pBuf = proc->pLineBuffer;
bufSize = proc->lineBufferSize;
}
else {
pBuf = (char[256]){};
bufSize = 256;
}
while (fgets(pBuf, bufSize, fp) != NULL) { // 3
switch (proc->processLine(pThis, pBuf)) { // 4
case LINE_PROCESS_CANCEL:
return true;
case LINE_PROCESS_ERROR:
return false;
default:
break;
}
}
return feof(fp); // 5
}
|
- 1readAsTextLines関数を使用する場合、FileAccessor構造体のpDataメンバーには、LineProcessor構造体へのポインターを設定する。このため、ここではキャストによってLineProcessor構造体へのポインターに変換している。
- 2アプリケーションからlineBufferSizeメンバーとして0以外の値が渡された場合は、行バッファーとしてアプリケーションが指定したものを使用する。そうでなければライブラリの中で256bytesの領域を確保して用いる。
- 3fgets()関数を用いて行を読み込むループ。エラーが起きるかファイルを最後まで読み込み終わるまでループする。
- 4アプリケーションが用意したprocessLine関数(ここではその実体はリスト5で定義しているreadLine関数)を実行し、戻り値がLINE_PROCESS_CANCEL値(正常)ならtrueを、LINE_PROCESS_ERROR値(エラー)ならfalseを返し、それ以外(LINE_PROCESS_CONTINUE値)ならばswitch文を抜けてループを続ける。
- 5ループを抜けた場合はエラーかEOFなので、feof関数で判定しエラーの場合はfalseを、EOFならtrueを返す。
なお複合リテラルを使用する場合、スタックの深さに注意が必要だ。例えば以下のようなコードがあった場合を考えてみよう(リスト8)。
typedef struct {
int i;
int j;
} FOO;
void foo(FOO *p) {
}
int main() {
foo(&(FOO) {
.i =123,
.j =234,
});
foo(&(FOO) {
.i =345,
.j =456,
});
}
|
これをgcc 4.8.1でコンパイルした場合、アセンブリコードは以下のようになった(リスト9。コンパイルオプションに-Sを付けることでアセンブリコードを確認できる)。
……省略……
subq $32, %rsp # 作業領域を32byte確保
movl $123, -32(%rbp) # メンバーiを設定
movl $234, -28(%rbp) # メンバーjを設定
leaq -32(%rbp), %rax # fooに渡すポインター
movq %rax, %rdi
call foo # foo関数の呼び出し
movl $345, -16(%rbp) # メンバーiを設定
movl $456, -12(%rbp) # メンバーjを設定
leaq -16(%rbp), %rax # fooに渡すポインター
movq %rax, %rdi
call foo # foo関数の呼び出し
……省略……
|
つまりコードとしては、リスト10をコンパイルしたのと同じ結果になっている。
……省略……
int main() {
FOO arg1 = {
.i =123,
.j =234,
};
foo(&arg1);
FOO arg2 = {
.i =345,
.j =456,
};
foo(&arg2);
}
|
これは複合リテラルのスコープを考えれば当然だ。複合リテラルは、それを含む最も内側のブロックがスコープとなるので、main関数内で有効となる必要がある。このため複合リテラルを用いた呼び出しが2つ以上あると、ライブラリへの引数を単純に関数の引数で渡す場合と比べてスタックを多めに消費してしまうことになる。従って適切に関数分解を行って、あまり多くのライブラリ呼び出しが1つの関数内に入らないように注意するか、静的に複合リテラルを生成する必要がある。なお試しにリスト11のようにブロックを意図的に狭めてみたが、スタック消費量に違いはなかった(また、clang 3.2-7ubuntu1を用いた場合には、リスト8の状態でも最適化されてスタックは16byteしか消費しなかった)。
……省略……
int main() {
{
foo(&(FOO) {
.i =123,
.j =234,
});
}
{
foo(&(FOO) {
.i =345,
.j =456,
});
}
}
|
デフォルト値を指定できるようにする
今回作成したリソース管理ライブラリはデフォルトの動作がいくつか用意されており、エラー処理を指定しなければ、標準エラー出力にメッセージを出力するようになっている。このデフォルトの挙動が、たまたま状況に合っていればライブラリを呼び出すときの記述を大幅に省略できるが、合っていなければ毎回多量の指定を行わなければならなくなる。今度は、このデフォルトを変更できるようにしてみよう。
デフォルトを変更する方法で最も簡単なのはグローバル変数を使用する方法だが、これはマルチスレッドと相性が悪く、またコードの独立性を損なってしまう。今回はグローバル変数を使用しない方法を考えてみることにしよう。まずデフォルト値を渡すことができる関数を用意する(リスト12)。
static bool doNothingProcess(FileAccessor *pThis, FILE *fp);
const FileAccessor DEFAULT_FILE_ACCESSOR_ARG = { // 5
.pName = "file",
.pMode = "r",
.onOpenError = openError,
.process = doNothingProcess, // 6
.onAccessError = accessError,
.onCloseError = closeError,
};
// 2
#define MERGE_ARG(member) \
(pAccessor->member ? pAccessor : pDefault)->member
void *accessFile(FileAccessor *pAccessor) { // 4
return accessFileWithDefault(pAccessor, &DEFAULT_FILE_ACCESSOR_ARG);
}
void *accessFileWithDefault(FileAccessor *pAccessor, const FileAccessor *pDefault) { // 1
accessFileDirect(&(FileAccessor) {
.pName = MERGE_ARG(pName),
.pMode = MERGE_ARG(pMode),
.onOpenError = MERGE_ARG(onOpenError),
.process = MERGE_ARG(process),
.onAccessError = MERGE_ARG(onAccessError),
.onCloseError = MERGE_ARG(onCloseError),
.pData = MERGE_ARG(pData),
});
}
void *accessFileDirect(FileAccessor *pAccessor) { // 3
FILE *fp = fopen(pAccessor->pName, pAccessor->pMode);
if (! fp) {
(pAccessor->onOpenError)(pAccessor, errno);
return pAccessor->pData;
}
if (! pAccessor->process(pAccessor, fp))
(pAccessor->onAccessError)(pAccessor, errno);
if (fclose(fp) == EOF) {
(pAccessor->onCloseError)(pAccessor, errno);
}
return pAccessor->pData;
}
static bool doNothingProcess(FileAccessor *pThis, FILE *fp) { // 6
return true;
}
|
- 1デフォルト値を受けられるようにした関数である。FileAccessor構造体の各メンバーについて、pAccessor側に値が設定されていなければ、pDefault側から値を取得して構成し直し、accessFileDirect関数を呼び出す。
- 21の実装を簡単にするためのマクロ。
- 31から呼び出しているaccessFileDirect関数。これまでのaccessFile関数とほぼ同じ処理になっているが、デフォルト値は外から渡せるようにしたため、関数ポインターのNULLチェックを省略している。
- 4これまでのaccessFile関数は、accessFileWithDefault関数の第2引数にDEFAULT_FILE_ACCESSOR_ARG値を指定して呼び出すように変更してある。
- 5ライブラリにおけるデフォルト値。呼び出し元が指定を省略したときに使用するデフォルト値が設定されている。
- 6process変数に何も設定されなかった場合のために、doNothingProcess関数をデフォルトとして用意している。
もう1つ、デフォルト値を簡単に構成できるようにヘルパー関数を用意した(リスト13)。
FileAccessor *mergeAccessFileArgs(FileAccessor *pAccessor, const FileAccessor *pDefault) {
pAccessor->pName = MERGE_ARG(pName);
pAccessor->pMode = MERGE_ARG(pMode);
pAccessor->onOpenError = MERGE_ARG(onOpenError);
pAccessor->process = MERGE_ARG(process);
pAccessor->onAccessError = MERGE_ARG(onAccessError);
pAccessor->onCloseError = MERGE_ARG(onCloseError);
pAccessor->pData = MERGE_ARG(pData);
return pAccessor;
}
|
この関数は、引数pAccessorで指定したFileAccessor構造体のメンバーの中で設定されていないものに対し、引数pDefaultの対応するメンバーをコピーした後に、pAccessorを返す。これを用いることで2つの引数をマージできる。main.cファイルは以下のように書き直せる(リスト14)。
static FileAccessor HELLO_FILE_ACCESSOR_ARG; // 1
static void *accessHello(FileAccessor *p) { // 3
return accessFileWithDefault(p, &HELLO_FILE_ACCESSOR_ARG);
}
int main(int argc, char **argv) {
HELLO_FILE_ACCESSOR_ARG = *mergeAccessFileArgs // 2
(&(FileAccessor) {
.pName = "hello.txt",
},
&DEFAULT_FILE_ACCESSOR_ARG);
accessHello(&(FileAccessor) { // 4
.pMode = "w",
.process = writeAsText,
.pData = "Hello\nWorld\n",
});
accessHello(&(FileAccessor) {
.pMode = "r",
.process = readAsTextLines,
.pData = &(LineProcessor) {
.processLine = readLine,
}
});
}
|
- 1pNameメンバーの値を「hello.txt」にしたデフォルト値を作成する。このデフォルト値を格納するための構造体を用意している。
- 21で作成した格納場所にデフォルト値を作成する。mergeAccessFileArgs関数を用いて、ライブラリのデフォルト値を基にして、「pName = "hello.txt"」を追加している。
- 3hello.txtファイルを操作するための関数。この関数ではデフォルト値としてHELLO_FILE_ACCESSOR_ARG値を指定している。
- 43で用意した関数を用いることで、pNameメンバーの指定を省略できる。
まとめ
今回はC99の仕様を日々の開発に積極的に用いることで、コードがどのように変わるか、その可能性を探ってみた。題材としてはリソース管理ライブラリを用い、まずANSI-Cで書かれた一般的なファイル書き込みコードを検証した。こうしたコードの課題として以下のような点が挙げられることを確認した。
- エラー処理を忘れやすい
- fopen関数だけでなく、fputs関数のようなファイルにアクセスする関数、fclose関数のようなファイルをクローズする関数に対してもエラー処理を行わないと、エラーが起きたことを見逃してしまう可能性がある。 - 特定条件の際にクローズ処理を忘れやすい
- ファイルのオープンには成功していて、その後のアクセスの際にエラーが起きた際にクローズを忘れやすい。これは特定条件のときにだけリソースリークを招くため、テストで発見することが難しい。 - エラーが起きたときに必要な情報を確実に残す必要がある
- エラーが起きたときに、後で解析できるように必要な情報をきちんと残すようにコーディングする必要があるが、これを徹底することは難しい。
こうした課題に対処するためリソースを扱うコードを考察し、その中から定型処理と非定型処理とを特定した。その上で、今回は関数ポインターを用いて非定型処理を渡すようにすることで、ライブラリ側で定型処理部分を実装しつつ、非定型処理をその中に差し込めるようにした。ライブラリの呼び出し引数には複合リテラルを用いることにより、以下のようなメリットが得られることが分かった。
- 引数が多量にある場合にも、どの引数が何を意味するのか分かりやすい
- 指定を省略した場合に適用されるデフォルトの引数を提供できる。また異なるデフォルト値を用意することもできる
一方で、複合リテラルを自動記憶域で用いる場合、処理系によってはブロック内の複数の複合リテラルが同時にスタックに乗る場合があるのでスタックの消費に注意が必要であることが分かった。
他のプログラミング言語には、同様の機能を提供する名前付き引数、デフォルト引数といった言語機能を持ったものがあるが、Cでも最新の言語仕様を用いることで、同じような記述が可能となることが分かる。C99の仕様は多くの処理系でサポートされつつあるので、ぜひ日々の開発に役立てみてほしい。