Deep Insider の Tutor コーナー
>>  Deep Insider は本サイトからスピンオフした姉妹サイトです。よろしく! 
C言語の最新事情を知る(4)

C言語の最新事情を知る(4)

C99でリソース管理ライブラリを作ってみる

2014年6月2日

前回まではC99/C11の仕様について見てきた。今回は、従来のCプログラミングが、C99の仕様を活用することで、どのように変化するのかを取り上げる。

花井 志生
  • このエントリーをはてなブックマークに追加

 これまで、C99とC11の仕様について主だったところを紹介してきた。今回は応用編として、C99の仕様を用いることで日々のプログラミングがどのくらい簡素化、あるいは分かりやすくできるか、その可能性を探ってみたい。

リソース管理ライブラリを作る

 Cではリソース管理はプログラマーに委ねられている。fopen関数でオープンしたファイルは確実にfclose関数で閉じなければならないし、malloc関数で確保したメモリはfree関数で確実に解放しなければならない。これは時としてリソースリーク(=リソースの解放し忘れ)という面倒なバグを引き起こす。リスト1は典型的なファイルにテキストを書き込むコードである。

ANSI-C
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));
  }
}
リスト1 ファイルにテキストを書き込むコード(legacy.cファイル)

 このコードにはいくつかの考慮点がある。

エラー処理

 一般に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関数の呼び出し時になる

エラーの際のクローズ処理

 よくある間違いとして、ファイルアクセスを行う関数の実装時に、fputs関数などのアクセスエラーを検出したとき、fclose関数を呼び出すのを忘れてしまうというケースがある。この場合、fopen関数が成功しているのでファイルはオープンされたままになっている。コーディング段階では、ファイルハンドルに余裕があるのでこの間違いには気付きにくい。しかし、この関数が定期的に呼び出されるようなものだと、ファイルアクセスエラーが解消されない限り、この関数は「どんどんとファイルハンドルを浪費して最終的にシステム全体が動作しなくなる」という障害を引き起こすことがある。前述のリスト1ではfputs関数でエラーが起きたときにも、fclose関数が呼ばれるようになっていることが分かる。

エラー原因の表示

 リスト1では、エラーが起きたときにファイル名やモード、errno変数が示すエラーを表示しているが(さらには__FILE__マクロや__LINE__マクロを含めることが多い)、実際のコードではエラー時に十分な情報が書き出されていないことも少なくない。このようなケースでは、特に障害がまれにしか起きず、それも特定の環境でしか起きない場合、例え単純なバグでも障害の原因特定に膨大な時間を要してしまうことがある。

 このようにリソース管理にはいろいろと考慮点が多く、個々の開発者にこうした配慮を求めるのは負担が大きい。今回はリソース管理をより簡単にするためのライブラリをC99の仕様を活用して作成してみたい。

リソース管理ライブラリの仕組み

 リソース管理が厄介なのは、前節で見た通り、場面によって変化する処理と変化しない定型処理とが入り組んでいるためである(図1)。

図1 リソースアクセス処理
図1 リソースアクセス処理

 ファイルアクセスの全体の流れや、オープン処理、クローズ処理といった部分は共通であるのに対し、エラー処理や実際にオープンされたファイルの処理は場面によってさまざまに変化する。今回は、定型処理はライブラリ側に任せつつ、変化する処理を関数ポインターで外出しにして変更を可能としてみよう。以下は今回作成するリソース管理ライブラリを使用し、「hello.txt」という名前のファイルに、文字列「"Hello\nWorld\n"」を書き込む例である(リスト2)。

C99
#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",
  });
}
リスト2 リソース管理ライブラリを使用した例(main.cファイル)

 通常であれば、リスト1に示したように、ファイルのオープン、クローズやエラー処理が書かれているはずが、リスト2ではaccessFile関数の呼び出しのみで完結していることが分かる。1の部分ではC99で追加された複合リテラルを用いている。ライブラリに渡すデータが多い場合、それらをそのまま関数の引数として渡すように設計すると、ずらりと大量の引数が並び、それぞれの引数が何を意味するのか分かりにくくなる。複合リテラルを用いるとメンバー名を指定できるため、それぞれの引数が何を意味するのか分かりやすくなる。もちろんメンバー名は省略しても構わないが、ここはあえて書くことで、それぞれの引数が何を意味しているのか明確にするのが狙いである。

 ここでは複合リテラルを生成した後に、そのポインターを関数に渡しているが、ここで生成された複合リテラルのスコープはどこまで有効なのだろうか? 実際にaccessFile関数が実行されるときまで有効なのだろうか? 複合リテラルが自動記憶域で生成された場合、それは直近のブロック内で有効となることが仕様書に示されている。従って今回の場合はmain関数内で有効となるので問題無い。

 次にFileAccessor構造体とwriteAsText関数の宣言を見てみよう(リスト3)。

C99
#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
リスト3 リソース管理構造体(file_accessor.hファイル)

 FileAccessor構造体は今回使用する構造体で、以下のようなメンバーを持っている。

メンバー説明
pName アクセスするファイル名
pMode アクセスするモード
onOpenError ファイルオープンに失敗したときに呼び出す処理
process ファイルが開かれている間に実行する処理
onAccessError processの処理で失敗したときに呼び出す処理
onCloseError ファイルのクローズに失敗したときに実行する処理
pData アプリケーションで自由に使用できるデータ
FileAccessor構造体のメンバー

 writeAsText関数はFileAccessor構造体のprocessメンバーに指定できる関数で、構造体のpDataメンバーで指されたデータをテキストと見なしてファイルに書き込む関数である(FileAccessor構造体のprocessメンバーと同じ引数、戻り値型になっていることに注意してほしい)。

 それでは最後にaccessFile関数と、writeAsText関数を見てみよう(リスト4)。

C99
#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;
}
リスト4 リソース管理ライブラリ(file_accessor.cファイル)
  • 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)。

C99
……省略……
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,
    }
  });
}
リスト5 ファイルの読み出し例(main.cファイル)
  • 1今回は改行で区切られたテキストファイルを読み込むことを想定している。ライブラリはファイルをテキストとして読み込みながら、読んだテキスト1行を、アプリケーションに渡す。これを実現するためアプリケーションから、行を処理するための関数をライブラリに渡せるように、LineProcessorという構造体へのポインターをpDataメンバーで受け取るようになっている。
  • 2アプリケーションが用意した1行処理のための関数である。今回は単に受け取った1行をfgets関数で標準エラーに書き出してから、LINE_PROCESS_CONTINUEという値を返している。この戻り値をライブラリが受け取るとライブラリは次の行の読み出しに移る。結果として常にLINE_PROCESS_CONTINUE値を返すようにしておけば全行を処理できることになる。

 それでは今回使用している構造体、定数の宣言を見てみよう(リスト6)。

C
……省略……
 
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);
リスト6 リソース管理構造体に読み込み処理を追加(file_accessor.hファイル)
  • 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)。

C99
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
}
リスト7 テキストファイル読み込み処理(file_accessor.cファイル)
  • 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)。

C99
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,
  });
}
リスト8 スタックの深さに注意

 これを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関数の呼び出し
……省略……
リスト9 リスト8のコンパイル結果

 つまりコードとしては、リスト10をコンパイルしたのと同じ結果になっている。

C99
……省略……
int main() {
  FOO arg1 = {
    .i =123,
    .j =234,
  };        
 
  foo(&arg1);
 
  FOO arg2 = {
    .i =345,
    .j =456,
  };        
 
  foo(&arg2);
}
リスト10 リスト8と等価なコード

 これは複合リテラルのスコープを考えれば当然だ。複合リテラルは、それを含む最も内側のブロックがスコープとなるので、main関数内で有効となる必要がある。このため複合リテラルを用いた呼び出しが2つ以上あると、ライブラリへの引数を単純に関数の引数で渡す場合と比べてスタックを多めに消費してしまうことになる。従って適切に関数分解を行って、あまり多くのライブラリ呼び出しが1つの関数内に入らないように注意するか、静的に複合リテラルを生成する必要がある。なお試しにリスト11のようにブロックを意図的に狭めてみたが、スタック消費量に違いはなかった(また、clang 3.2-7ubuntu1を用いた場合には、リスト8の状態でも最適化されてスタックは16byteしか消費しなかった)。

C99
……省略……
int main() {
  {
    foo(&(FOO) {
      .i =123,
      .j =234,
    });
  }
 
  {
    foo(&(FOO) {
      .i =345,
      .j =456,
    });
  }
}
リスト11 ブロックを意図的に狭めてみたコード

デフォルト値を指定できるようにする

 今回作成したリソース管理ライブラリはデフォルトの動作がいくつか用意されており、エラー処理を指定しなければ、標準エラー出力にメッセージを出力するようになっている。このデフォルトの挙動が、たまたま状況に合っていればライブラリを呼び出すときの記述を大幅に省略できるが、合っていなければ毎回多量の指定を行わなければならなくなる。今度は、このデフォルトを変更できるようにしてみよう。

 デフォルトを変更する方法で最も簡単なのはグローバル変数を使用する方法だが、これはマルチスレッドと相性が悪く、またコードの独立性を損なってしまう。今回はグローバル変数を使用しない方法を考えてみることにしよう。まずデフォルト値を渡すことができる関数を用意する(リスト12)。

C
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;
}
リスト12 デフォルトを受けられるようにしたaccessFile関数(file_accessor.cファイル)
  • 1デフォルト値を受けられるようにした関数である。FileAccessor構造体の各メンバーについて、pAccessor側に値が設定されていなければ、pDefault側から値を取得して構成し直し、accessFileDirect関数を呼び出す。
  • 21の実装を簡単にするためのマクロ。
  • 31から呼び出しているaccessFileDirect関数。これまでのaccessFile関数とほぼ同じ処理になっているが、デフォルト値は外から渡せるようにしたため、関数ポインターのNULLチェックを省略している。
  • 4これまでのaccessFile関数は、accessFileWithDefault関数の第2引数にDEFAULT_FILE_ACCESSOR_ARG値を指定して呼び出すように変更してある。
  • 5ライブラリにおけるデフォルト値。呼び出し元が指定を省略したときに使用するデフォルト値が設定されている。
  • 6process変数に何も設定されなかった場合のために、doNothingProcess関数をデフォルトとして用意している。

 もう1つ、デフォルト値を簡単に構成できるようにヘルパー関数を用意した(リスト13)。

C
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;
}
リスト13 デフォルト値を構成するためのヘルパー関数(file_accessor.cファイル)

 この関数は、引数pAccessorで指定したFileAccessor構造体のメンバーの中で設定されていないものに対し、引数pDefaultの対応するメンバーをコピーした後に、pAccessorを返す。これを用いることで2つの引数をマージできる。main.cファイルは以下のように書き直せる(リスト14)。

C
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,
    }
  });
}
リスト14 デフォルトを作成してライブラリを呼び出す
  • 1pNameメンバーの値を「hello.txt」にしたデフォルト値を作成する。このデフォルト値を格納するための構造体を用意している。
  • 21で作成した格納場所にデフォルト値を作成する。mergeAccessFileArgs関数を用いて、ライブラリのデフォルト値を基にして、「pName = "hello.txt"」を追加している。
  • 3hello.txtファイルを操作するための関数。この関数ではデフォルト値としてHELLO_FILE_ACCESSOR_ARG値を指定している。
  • 43で用意した関数を用いることで、pNameメンバーの指定を省略できる。

まとめ

 今回はC99の仕様を日々の開発に積極的に用いることで、コードがどのように変わるか、その可能性を探ってみた。題材としてはリソース管理ライブラリを用い、まずANSI-Cで書かれた一般的なファイル書き込みコードを検証した。こうしたコードの課題として以下のような点が挙げられることを確認した。

  • エラー処理を忘れやすい
     - fopen関数だけでなく、fputs関数のようなファイルにアクセスする関数、fclose関数のようなファイルをクローズする関数に対してもエラー処理を行わないと、エラーが起きたことを見逃してしまう可能性がある。
  • 特定条件の際にクローズ処理を忘れやすい
     - ファイルのオープンには成功していて、その後のアクセスの際にエラーが起きた際にクローズを忘れやすい。これは特定条件のときにだけリソースリークを招くため、テストで発見することが難しい。
  • エラーが起きたときに必要な情報を確実に残す必要がある
     - エラーが起きたときに、後で解析できるように必要な情報をきちんと残すようにコーディングする必要があるが、これを徹底することは難しい。

 こうした課題に対処するためリソースを扱うコードを考察し、その中から定型処理と非定型処理とを特定した。その上で、今回は関数ポインターを用いて非定型処理を渡すようにすることで、ライブラリ側で定型処理部分を実装しつつ、非定型処理をその中に差し込めるようにした。ライブラリの呼び出し引数には複合リテラルを用いることにより、以下のようなメリットが得られることが分かった。

  • 引数が多量にある場合にも、どの引数が何を意味するのか分かりやすい
  • 指定を省略した場合に適用されるデフォルトの引数を提供できる。また異なるデフォルト値を用意することもできる

 一方で、複合リテラルを自動記憶域で用いる場合、処理系によってはブロック内の複数の複合リテラルが同時にスタックに乗る場合があるのでスタックの消費に注意が必要であることが分かった。

 他のプログラミング言語には、同様の機能を提供する名前付き引数、デフォルト引数といった言語機能を持ったものがあるが、Cでも最新の言語仕様を用いることで、同じような記述が可能となることが分かる。C99の仕様は多くの処理系でサポートされつつあるので、ぜひ日々の開発に役立てみてほしい。

C言語の最新事情を知る(4)
1. C99の仕様

長い歴史を持ちながら、依然として人気の高いC言語。その最新仕様の情報にキャッチアップするための連載スタート。今回は1999年に策定された「C99」を取り上げる。

C言語の最新事情を知る(4)
2. C11の仕様-脆弱性対応に関連する機能強化点

C言語の最新仕様の情報にキャッチアップしよう。C11の仕様で強化された機能のうち、主に脆弱性対応に関連するものを紹介する。

C言語の最新事情を知る(4)
3. C11の仕様-それ以外の主な機能強化点

C言語の最新仕様の情報にキャッチアップしよう。C11の仕様で強化された機能のうち、脆弱性対応以外の主要な強化点を紹介。

C言語の最新事情を知る(4)
4. 【現在、表示中】≫ C99でリソース管理ライブラリを作ってみる

前回まではC99/C11の仕様について見てきた。今回は、従来のCプログラミングが、C99の仕様を活用することで、どのように変化するのかを取り上げる。

サイトからのお知らせ

Twitterでつぶやこう!