
[update 2001/11/23]
CSVとは、バージョン管理システムで、プログラムのソースファイルの変遷を記録する時に良く使われます。ソフトウェアを変更したことでバグが発生しても、古いバージョンを簡単に取り出せるので便利なのです。
更に、複数人で開発する場合にも素晴らしい効果を発揮します。他の人が修正したファイルを間違って上書きしてしまうこともなくなるのです。
って、それはCVSだ〜〜!ボケ長すぎ。
良く間違えるんですよね。CVSを使おうとして、間違ってCSVってタイプしてしまって、「あれっ?」って。
CとVは同じ指を使ってタイプするので、CからVへ指が移動している間に、ついSを打ってしまうのです。
それは兎も角…、本題。
CSVファイルとは、comma separate value という名前通り、データをコンマで区切って並べてあるテキストファイルのことです。
データを保存する形式としては最も簡単な部類に入り、またテキストファイルであることから普通のテキストエディタで簡単に修正が出来るという性質があるので、なにかと良く使われます。
という事で、CSVファイルを扱うC++クラスを作ってみました。簡易版です。
CSVは良く使われる形式なので、探せばもっと出来の良い高機能なものが見つかることでしょう。
[ ▲ 先頭 ]
[update 2003/12/13]
ある目的で作っていたけど、結局使わなかったのでココで公開しちゃいます。
N進数でM桁までカウントすることの出来るカウンタです。+1することしか出来ません。
まぁ、テンプレートの練習という事で。
【ソースファイル】 (サーバの都合で拡張子が変えてあります)
[update 2003/12/13]
Cでプログラムを組んでいると、何か処理を行ったら、必ず終了関数を呼ばなければいけない場合は良くあります。
例えば、malloc()したらfree()を呼ばないと行けないとか、fopen()したらfclose()だとか。
やれば良いじゃん。
と、言ってしまえばそれまでですが、終了関数を呼ぶのを忘れる事も良くあります。
気をつけていても、途中で例外が発生したりすると結構面倒だったりします。
C++にもJavaのfinallyの様な構文が欲しくなります。
クラスを作って、デストラクタで終了関数を呼ぶようにするわけですが、Javaにある内部無名クラスの様な構文を持たないC++では、わざわざ作るのも面倒だったりします。
と言うわけで、関数のポインタで終了関数を指定できるようにすれば、汎用的な物が作れそうです。
{
AutoCloser<FILE*, int> fp(fclose);
fp = fopen("test", "w");
fputc(1, fp);
// 色々と処理する
// ここでfcloseが勝手に呼ばれる。
// 例外を投げても大丈夫
}
auto_ptrで解体用の関数を指定できるようになっているものって、既にあると思いますが。
【ソースファイル】 (サーバの都合で拡張子が変えてあります)
[update 2004/2/1]
ある整数を、別の整数Nの倍数に繰り上げます。
例えば、3の倍数への繰上げ操作は、1→3、2→3、3→3、4→6となります。
// nをNの倍数に繰り上げる。
// その1
unsigned int round_up(unsigned int n, unsigned int N)
{
if (n % N == 0)
return n;
else
return n + (N - (n % N));
}
負の数を考えると面倒なので、unsigned です。Nが0の時も取り合えず考えません。
さて、
最近のCPUは、条件分岐があるとパイプラインが乱れ遅くなる可能性があるので、できれば条件分岐を無くしたい所です。
// その2
unsigned int round_up(unsigned int n, unsigned int N)
{
return (n - 1) + (N - ((n - 1) % N));
}
これだと、n=0の時に、n-1が-1になって巧くいきません。
// その2-1
unsigned int round_up(unsigned int n, unsigned int N)
{
return (n + (N - 1)) - ((n + (N - 1)) % N);
}
n+N-1が0xffffffff以上になると(intが32bitを想定)、やっぱり巧くいかないので、扱う値の範囲によって使い分けましょう。
Cマガジン2003年5月号の「あっぱれご意見番」でも記事になっていますが、その1のコードの方がロジックが単純なので良いと言う意見もあります。
さてさて、
割り算と言うのは、他の演算に比べて処理が遅いのです。出来れば割り算を使わずに済ませたい所です。
“Nを2のべき乗”と限定すれば、(n + (N - 1)) & ~(N - 1)で求めることができるのを利用しましょう。
// その3
unsigned int round_up(unsigned int n, unsigned int N)
{
if (N & (N - 1)) // 2のべき乗判定
return (n + (N - 1)) - ((n + (N - 1)) % N);
else
return (n + (N - 1)) & ~(N - 1);
}
折角無くした条件分岐が復活しているのはご愛嬌。
さてさてさて、
実際にプログラムを組む場合、繰り上げる基準となるNが固定となっている事が多いので、テンプレートを使うと都合が良くなります。
// その4
template<unsigned int N>
unsigned int round_up(unsigned int n)
{
return (n + (N - 1)) - ((n + (N - 1)) % N);
}
Nが2のべき乗の場合は、テンプレートの特殊化を使って実装します。intが32bitなら特殊化する値も32個なのでマクロを使って対処しましょう。
// その4の続き
// 0はエラーなので、本体は実装しない
template<>
unsigned int round_up<0>(unsigned int n);
// 1は、そのまま
template<>
unsigned int round_up<1>(unsigned int n)
{
return n;
}
// 2のべき乗用の関数テンプレートを作るマクロ
#define RoundUp(N) \
template<> \
unsigned int round_up<N>(unsigned int n) \
{ \
return (n + (N - 1)) & ~(N - 1); \
}
// マクロを使って特殊化された関数テンプレートを作る
RoundUp(2)
RoundUp(4)
RoundUp(8)
...
RoundUp(0x80000000)
Nが固定であるという制限がありますが、2のべき乗かそうでないかで、良い感じにコードが切り替わります。さらにN=0をコンパイル時にエラーに出来ます。
さてさてさてさて、
折角ここまできたので、もうちょっと遊んでみましょう。
// その5 … 失敗
template<unsigned int N>
struct check_N_is_pow2{
enum {
value = (N & (N - 1)),
};
};
template<unsigned int N>
unsigned int round_up(unsigned int n)
{
return round_up_sub<check_N_is_pow2<N>::value, N>(n);
}
template<>
unsigned int round_up<1>(unsigned int n)
{
return n;
}
template<>
unsigned int round_up<0>(unsigned int n);
template<int TYPE, unsigned int N>
unsigned int round_up_sub(unsigned int n)
{
return (n + (N - 1)) - ((n + (N - 1)) % N);
}
template<unsigned int N>
unsigned int round_up_sub<0, N>(unsigned int n) // (1)
{
return (n + (N - 1)) & ~(N - 1);
}
こうすれば、intのビット数が変わっても、その4の様にマクロを変更しなくても良いので、便利かと思ったんですが、(1)の所でコンパイルエラー(Visual C++ .NET 2003で)。
どうやら、関数テンプレートの部分特殊化は、駄目なようです。残念。
情報源:http://www.tietew.jp/cppll/archive/9010(cppll MLのアーカイブ)
[update 2004/8/4]
CSV形式のデータというのは、幾つかのデータを「,」で連結して並べたものです。
例えば「a,b,c」の様に。
このCSV形式のデータを元のデータに分解する、次の様な関数を考えます。
vector<string> fromCSV(const string& csv)
{
...
}
文字列を特定の文字で分割するにはC言語の標準関数strtok()が使えます。
多分コレが一番楽でしょう。
vector<string> fromCSV1(const string& csv)
{
vector<string> data;
string temp = csv;
char* s = strtok(const_cast<char*>(temp.c_str()), ",");
if (s != NULL) {
data.push_back(s);
while ((s = strtok(NULL, ",")) != NULL)
data.push_back(s);
}
return data;
}
strtok()は元の文字列を書き換えるので、こうなってますけど、stringが抱えているバッファに対してconst外しをしているのはちょっと乱暴ですかね。
この関数の最大の問題点は、「,」が出現すれば問答無用にそこでぶった切るところでしょう。
そのため「a」「b1,b2」「c」の様にデータの途中に「,」を含むものは扱えないという事です。
ちなみに、コレを何も考えずに「,」で繋ぐと「a,b1,b2,c」となり、元の形式を特定できなくなってしまうので、「,」を含む場合は「"」で囲みます。
こんな感じ「a,"b1,b2",c」。
それじゃあ、今度は「"」を含む場合はどうするんでしょう。
その場合は、「"」を「\"」に置き換えます。
そして「\」は「\\」で表すようにします。
これが、比較的良く使われるCSV形式です。
「a」「b1,\,",b2」「c」をCSV形式で表すと「a,"b1,\\,\",b2",c」になります。
これを、前述のプログラムで分解すると、(何も考えずに「,」で切るので)「a」「"b1」「\\」「\"」「b2"」「c」になってしまいます。
正しく分解するためには、それなりのプログラムを書かないといけません。
って言うか、これって良く使うと思うんですけど、どうして標準で持っていないんでしょう。アレだけ大量のクラスを持つJavaや.NET frameworkにもありませんし。
まぁ、わざわざ作るのもアレなので、Boostにあるのを使いましょう。
Boostのescaped_list_separatorは、正にこのためにあるような代物です。
vector<string> fromCSV2(const string& csv)
{
vector<string> data;
boost::tokenizer<boost::escaped_list_separator<char> > tok(csv);
copy(tok.begin(), tok.end(), back_inserter(data));
return data;
}
途端にコンパイル時間が長くなるという事さえ気にしなければ、これで完璧です。
なんて、簡単なんでしょう。
「a,"b1,\\,\",b2",c」は、ちゃんと「a」「b1,\,",b2」「c」となります。
と欧米人ならこれで済む所ですが………。
CSV形式のデータに漢字を含むと時々失敗します。
「a,"サシスセソ",c」を分割すると、「a」「サシスセ・,c」(「セ」の次の文字は文字化け)となってしまいます。
原因は「ソ」の2バイト目が「\」と同じ0x5cだから、「\」と間違えて処理してしまったからです(文字コードがShift JISの場合)。
この対策は3つ。
そもそもエスケープ記号にマルチバイト文字のコードと等しくなる文字「\」を使っている所為でおかしくなるのだから、エスケープ記号を重ならない別の文字に変更する。例えば「!」とか。
vector<string> fromCSV2_1(const string& csv)
{
vector<string> data;
boost::tokenizer<boost::escaped_list_separator<char> >
tok(csv, boost::escaped_list_separator<char>('!'));
copy(tok.begin(), tok.end(), back_inserter(data));
return data;
}
escaped_list_separatorはエスケープ文字や区切り記号を変更できるので、特にやらなければならない事はありません。便利です。
しかし、一般的なCSVは「\」エスケープなので、あまり良い解決法じゃないですね。
そこで、その2。
エスケープ記号がマルチバイト文字のコードと重複するのがいけないのなら、文字コードを変えてしまう。Unicodeを使うとか。
vector<string> fromCSV2_2(const string& csv)
{
vector<string> data;
wstring csv2 = convert_sjis_to_unicode(csv);
typedef boost::tokenizer<boost::escaped_list_separator<wchar_t>,
wstring::const_iterator,
wstring > unicode_tokenizer;
unicode_tokenizer tok(csv);
unicode_tokenizer::iterator it;
for (it = tok.begin(); it != tok.end(); ++it) {
data.push_back(convert_unicode_to_sjis(*tok));
}
return data;
}
Shift JISとUnicodeとの変換が面倒ならEUCでも可。
なんだったら、独自の文字コードだってOK。
要は「\」が(後「"」と「,」も)、漢字のコードと重ならなければ良いんですよ。
で、(今回の最終目的でもある)その3。
escaped_list_separatorを漢字対応にしてしまえ。
で、作ったのがescaped_list_separator_sjis。
class escaped_list_separator_sjis
{
private:
bool last_;
enum {
QUOTE = '\"', // 囲み記号
SEPARATOR = ',', // 区切り記号
ESCAPE = '\\', // エスケープ記号
};
// マルチバイトコードの1byte目判定
bool is_multibyte1(char c_)
{
unsigned char c = static_cast<unsigned char>(c_);
return ((c >= 0x81 && c <= 0x9f) || (c >= 0xe0 && c <= 0xfc));
}
// マルチバイトコードの2byte目判定
bool is_multibyte2(char c_)
{
unsigned char c = static_cast<unsigned char>(c_);
return ((c >= 0x40 && c <= 0x7e) || (c >= 0x80 && c <= 0xfc));
}
// エスケープ文字の処理
template <typename iterator, typename Token>
void do_escape(iterator& next, iterator end, Token& tok)
{
if (++next == end)
throw boost::escaped_list_error(std::string("cannot end with escape"));
switch (*next) {
case 'n':
tok += '\n';
break;
case QUOTE:
case SEPARATOR:
case ESCAPE:
tok += *next;
break;
default:
throw boost::escaped_list_error(std::string("unknown escape sequence"));
}
}
// マルチバイト文字の処理
template <typename iterator, typename Token>
void do_multibyte(iterator& next, iterator end, Token& tok)
{
tok += *next;
if (++next == end)
throw boost::escaped_list_error(std::string("cannot end with multi byte"));
if (is_multibyte2(*next)) {
tok += *next;
} else {
throw boost::escaped_list_error(std::string("unknown multi byte code"));
}
}
public:
escaped_list_separator_sjis(void) : last_(false)
{
}
void reset() {last_=false;}
template <typename InputIterator, typename Token>
bool operator()(InputIterator& next, InputIterator end, Token& tok)
{
bool bInQuote = false;
tok = Token();
if (next == end) {
if (last_) {
last_ = false;
return true;
} else {
return false;
}
}
last_ = false;
for ( ; next != end; ++next) {
switch (*next) {
case ESCAPE:
do_escape(next, end, tok);
break;
case SEPARATOR:
if (!bInQuote) {
++next;
last_ = true;
return true;
} else {
tok+=*next;
}
break;
case QUOTE:
bInQuote=!bInQuote;
break;
default:
if (is_multibyte1(*next)) {
do_multibyte(next, end, tok);
} else {
tok += *next;
}
break;
}
}
return true;
}
};
vector<string> fromCSV2_3(const string& csv)
{
vector<string> data;
boost::tokenizer<escaped_list_separator_sjis> tok(csv);
copy(tok.begin(), tok.end(), back_inserter(data));
return data;
}
escaped_list_separatorのコードをパクっ………参考に、マルチバイトコードの判定を追加してみました。
漢字対応とか言いながら、区切り記号に全角文字が使えなかったりするんですが、これでも十分使えますよね、ね、ね。[ ▲ 先頭 ]
[update 2004/9/29]
標準入力から1行入力します。
一番、簡単なのはコレでしょうか?
char* buff[100]; gets(buff);
ところが、この関数は、指定されたバッファのサイズを指定できません。
上の例では100文字以上ある行を入力すると、最近なにかと話題のバッファオーバーフローを起こします。
そのため、gets()は使ってはいけない関数に認定されました。
替わりに使う事が推奨されているのがfgets()です。
char* buff[100]; fgets(buff, 100, stdin);
バッファサイズが指定できるので(正しく指定してあれば)バッファオーバーフローは発生しません。
でも、バッファサイズ以上の文字列を入力すると、
そこで入力が止まってしまい、1行分を正しく入力できません。
fgets()は行末に達すると改行記号を追加するので、入力した文字列が改行記号で終わっているか判定することで、途中までしか入力していないかどうかを判定できます。
どのみち文末の改行記号は取り除かないといけませんし。
char* buff[N];
fgets(buff, N, stdin);
if (buff[strlen(buff) - 1] == '\n') {
buff[strlen(buff) - 1] = '\0';
// OK
} else if (strlen(buff) == N - 1) {
// 途中までしか入力していないので続きを読む
} else {
// OK (改行なしでEOFに達した)
}
で、大まかには、こんな感じになるんですが、1行がバッファに収まらなかった場合は、バッファを大きくしたりする処理をしなくちゃならないので、実は、結構面倒なのです。
C++では文字列をstd::string型で扱うので、どうせなら、std::string get_line(void)の様な関数があると便利です。
std::string get_line(void)
{
const int N = 100;
char buff[N];
std::string str;
for (;;) {
if (fgets(buff, N, stdin) == NULL)
break;
int len = strlen(buff);
if (buff[len - 1] == '\n') {
str.append(buff, len - 1);
break;
} else if (len == N - 1) {
str.append(buff);
} else {
str.append(buff);
break;
}
}
return str;
}
ところで、折角のC++なのですから
std::string str; std::cin >> str;
で、OKの様な気がしなくも無いのですが、これだと入力文字中にスペースがあるとそこで入力を切ってしまいます。
区切り記号を変更できないものか探してみたんですが、なさそうです。
std::string get_line(void)
{
std::string str;
char c;
while (std::cin >> std::noskipws >> c && c != '\n') {
str += c;
}
return str;
}
cin(というかistream)にはgetline()というメンバ関数があるんですが、直接string型の値を返すんじゃなくて、fgets()の様に、配列とそのサイズを指定するんですよね。
とりあえず、そっちも使ってみましょう
std::string get_line(void)
{
std::string str;
const int N = 100;
char buff[N];
for (;;) {
std::cin.getline(buff, N);
str += buff;
if (std::cin.rdstate() == std::ios_base::failbit) {
std::cin.clear();
} else {
break;
}
}
return str;
}
[ ▲ 先頭 ]
[update 2004/12/30]
順列とはデータを順番に並べたものです。
n個のデータがある時、データの並べ方は全部でn!個あるんですが、その並べ方を全て列挙します。
イテレータ風になっているので(というかinput_iteratorとして実装してある)、次の様に使います。
int data[] = {0, 1, 2};
Permutation<int> it(data, 3);
Permutation<int> end;
for ( ;it != end; ++it) {
const int* p = *it;
copy(p, p + it.get_size(), ostream_iterator<int>(cout, ", "));
cout << std::endl;
}
これを実行すると
0, 1, 2, 0, 2, 1, 1, 0, 2, 1, 2, 0, 2, 0, 1, 2, 1, 0,
と表示されます。
あと、データが重複している場合のことは考えていません。
上のサンプルで
int data[] = {0, 0, 0};
とすると、
0, 0, 0,
が6回出力されます。
並べるデータの個数が増えると、並べ方の種類は、もの凄く多くなるので(10個のデータで3628800通り、20個のデータだと2432902008176640000通り)、その全てを列挙して何かの処理を行なうのは実用的じゃないんですけどね。
【ソースファイル】
(サーバの都合で拡張子が変えてあります)
【ソースファイル(C言語版)】
(サーバの都合で拡張子が変えてあります)
[ ▲ 先頭 ]
[update 2004/10/20]
順列とくれば今度は組み合せです。
組み合せはn個のものからm個を選び出す場合の選び方です。
int data[] = {0, 1, 2, 3};
Combination<int> c(data, 4, 2);
Combination<int> end;
for (int i = 0; c != end; ++c, i++) {
copy(*c, *c + c.get_size(), ostream_iterator<int>(cout, ", "));
cout << endl;
}
結果は、こうなります。
0, 1, 0, 2, 0, 3, 1, 2, 1, 3, 2, 3,
やっぱり、データが重複している場合のことは考えていません。
【ソースファイル】 (サーバの都合で拡張子が変えてあります)
[ ▲ 先頭 ]
[update 2004/10/30]
上の順列と組み合わせを列挙するクラスを、データが重複している場合でも正しく列挙できるようにしてみました。
データとして、{0, 0, 1}を使用して順列を列挙すると
0, 0, 1 0, 1, 0 1, 0, 0
とだけ出力されます。
[ ▲ 先頭 ]
[update 2004/11/24]
Boostの乱数関係のライブラリは、かなり充実しています。
しかも、乱数生成のアルゴリズム(乱数生成エンジン)と、生成された値を加工するフィルタが分離されていて、それを自由に組み合せることが出来きます(と言っても、乱数生成エンジンでMersenne Twister以外を指定する場合って、あまりありませんけど…)。
// 1〜6の一様乱数生成 variate_generator<mt19937, uniform_smallint<> > rand(mt19937(), uniform_smallint<>(1,6));
// 平均50、分散10の正規分布に従う乱数生成 variate_generator<mt19937, normal_distribution<> > rand(mt19937(), normal_distribution<>(50.0, 10.0));
こうやって定義しておけば、後は
rand()
とやるだけで、定義時に指定した分布に従った乱数値が得られます。
便利です。
さてさてさて。
1つのプログラム中で、生成する乱数の分布が1種類しか無い場合は何も問題ないんですが、色々な分布の乱数を同時に使いたい時は、どうしましょうか。
variate_generator<略> rand1(mt19937(), uniform_smallint<>(1,6)); variate_generator<略> rand2(mt19937(), normal_distribution<>(50.0, 10.0));
このように、名前の違うオブジェクトを作って管理すればOKです。
ところが、
乱数生成エンジン(上の例だとmt19937)のオブジェクトが、複数作成されています。
mt19937 mt; variate_generator<略> rand1(mt, uniform_smallint<>(1,6)); variate_generator<略> rand2(mt, normal_distribution<>(50.0, 10.0));
このようにしても、rand1, rand2が持っているのはコピーなので、結局別々のオブジェクトを持つことになります。
で、そこまでやる意味があるのかどうか甚だ疑問なんですけど、1つの乱数エンジンを使いまわすようにするラッパーを作ってみました。
所要時間数分
template<typename T>
class BoostRandomAdapter
{
private:
T* engine;
public:
typedef typename T::result_type result_type;
public:
BoostRandomAdapter(T* _engine) : engine(_engine)
{
}
result_type min(void) const
{
return engine->min();
}
result_type max(void) const
{
return engine->max();
}
result_type operator()()
{
return (*engine)();
}
};
template<typename T>
BoostRandomAdapter<T> boost_random_adapter(T* engine)
{
return BoostRandomAdapter<T>(engine);
}
}
mt19937 mt; variate_generator<BoostRandomAdapter<mt19937>, uniform_smallint<> > rand1(BoostRandomAdapter<mt19937>(&mt), uniform_smallint<>(1,6)); variate_generator<略> rand2(BoostRandomAdapter<mt19937>(&mt), normal_distribution<>(50.0, 10.0));
これで、rand1とrand2で同じ(同一の)乱数エンジンを使いまわします。
ただし、乱数生成エンジンの生存期間(スコープ)をちゃんと意識しながら使わないといけないので、使い勝手が………。
おまけ
GCC(g++ (GCC) 3.3.3 (cygwin special))を使うと何故か
parse error before numeric constant
というコンパイルエラーが出るので、こうしないといけないみたい。
mt19937 mt; BoostRandomAdapter<mt19937>(&mt) mt_adapter variate_generator<略> rand1(mt_adapter, uniform_smallint<>(1,6)); variate_generator<略> rand2(mt_adapter, normal_distribution<>(50.0, 10.0));おまけ、その2
型名を省略するためにアダプター関数boost_random_adapter()を作ってみました。
が、
結局variate_generatorの方に型名を書かないといけないのであまり変らないな…。
mt19937 mt; variate_generator<略> rand1(boost_random_adapter(&mt), uniform_smallint<>(1,6)); variate_generator<略> rand2(boost_random_adapter(&mt), normal_distribution<>(50.0, 10.0));
[ ▲ 先頭 ]
[update 2005/1/21]
C言語((C++に非ず)でオブジェクト指向を実践するには、(C++の)メンバ変数に相当する変数を構造体で一纏めにし、(C++の)メンバ関数に相当する関数に、構造体(のポインタ)を渡すようにします。
一番馴染みがあるのは、FILE型の変数とそれを使ったファイル操作関係の関数です。
で、そんな風に作ってある関数を、C++でも使うことがあります。
別に普通に使えば良いんですが、1つ不便な所があります。
C言語流オブジェクト指向にはデストラクタが無い。というかデストラクタに相当する関数(以後、「後始末関数」と呼ぶ)をスコープアウトした時に自動で呼ぶ仕組みが無い。
FILE* fp = fopen("test.txt", "r");
// (1)ココで例外を投げる
fclose(fp);
(1)の所で例外を投げてしまうと、fclose()は呼ばれず、ファイルが閉じられることなく、処理が続いています。
なので、後始末関数の呼び出しを自動化してしまおうというクラスを以前作ったんです。
FILE*をラップして、デストラクタでfclose()を呼ぶだけなので簡単です。
まぁ、類似の関数はファイル関係のもの以外にも沢山あり、その都度ラッパを作るのは面倒なので、テンプレートを使って1つにまとめてしまいましょうと言うのが、そもそもの発端です。
template<typename T, typename R>
class auto_handler
{
private:
T handler;
R (*close)(T);
public:
auto_handler(const T& _h, R (*_close)(T)) : handler(_h), close(_close) {}
~auto_handler() {close(handler);}
// キャスト演算子を用意しておくと、そのまま使えて便利
operator T&() {return handler;}
};
これが、以前作ったクラスを簡略化したものです。
FILEの場合は、こんな風に使えます。
「fopen()に失敗した時はどうなるんだ」と言うのは、とりあえず無視。
{
auto_handler<FILE*, int> fp(fopen("test.txt", "r"), fclose);
fgetc(fp);
// 例外を投げても大丈夫
} // ここでfpがスコープアウトして fclose()が自動で呼ばれる。
ところで、auto_handlerのテンプレート引数の2つ目(上の例だとint)が気になりませんか?
これは後始末を行なう関数(上の例だとfclose())の戻り値の型です。
fclose()だとint型、free()はvoid、Win32APIだと多くがBOOL型と後始末関数の戻り値の型がバラバラのため、“後始末関数へのポインタ”の型が決められず、仕方が無く付けてあるのです(さらに言うならば、Win32APIは、関数の型をR (__stdcall *)(T)としないといけないんだけど…)。
しかし、auto_handlerでは後始末関数の戻り値は全く利用していないし、普通は後始末関数の戻り値を気にしてプログラムを組むこともあまり無いし、言ってみればこれは「コンパイルを通すため」だけにある様な代物です。
(標準のstd::ifstreamだって、デストラクタでファイルのクローズに失敗しても、何もしませんから、決して悪い設計ではないんですよ、多分)
それに、後始末関数の戻り値なんて、普段気にしないから、当然覚えていなくて、わざわざ調べなければいけません(晶紀だけ?)。面倒です。
だから、この2つ目のテンプレート引数(後始末関数の戻り値の型)を削除できないものかと考えました。
とりあえず、思い付いたのが、この方法。
template<typename T>
class close_functor
{
public:
virtual void operator()(T&) = 0;
};
template<typename T, typename R>
class close_functor_impl : public close_functor<T>
{
private:
R (*close)(T);
public:
close_functor_impl(R (*_close)(T)) : close(_close) {}
virtual void operator()(T& h) {close(h);}
};
template<typename T>
class auto_handler
{
private:
T handler;
close_functor<T>* close;
public:
template<typename R>
auto_handler(const T& _h, R (*_close)(T)) : handler(_h)
{
close = new close_functor_impl<T, R>(_close);
}
~auto_handler()
{
(*close)(handler);
delete close;
}
operator T&() {return handler;}
};
ちゃんと、以下の様に使えます。
auto_handler<FILE*> fp(fopen("test.txt", "r"), fclose);
ここで止めておけば良いものを、「扱うデータが決まれば後始末関数も一意に決まる(FILEの時はfclose()という様に…)」事に気付いてしまい、それならば、
auto_handler<FILE*, fclose> fp = fopen("test.txt", "r");
と書けないものかと思ってしまったのです。
そうすると
typedef auto_handler<FILE*, fclose> FILE2;
FILE2 fp = fopen("test.txt", "r");
の様に、元の形に酷似したコードが書けます。
果たして、出来るのかっ!
(続く)
typedef auto_handler<FILE*, fclose> FILE2;
FILE2 fp = fopen("test.txt", "r");
とやって、fclose()を自動化したい。
そんなauto_handlerは、出来るのかっ!
正直、かなりてこずるかとも思ったんですが(だから2回に分けた。えっ、分れてない?)、不完全ながら、意外にあっさりと出来てしまいました。
typedef auto_handler<FILE*, int, fclose> FILE2;
FILE2 fp = fopen("test.txt", "r");
auto_handlerの2つ目のテンプレート引数はfclose()の戻り値の型です。
前回、指定しなくても良いようにしたのに、またしても復活してしまいました。
うが〜〜〜〜。
ちなみに、auto_handlerの実装は、こうなってます。
template<typename T, typename R, R (*close)(T)>
class auto_handler
{
private:
T handler;
public:
auto_handler(const T& _h) : handler(_h) { }
~auto_handler() {close(handler);}
operator T&() {return handler;}
};
随分、すっきりしました。
ところが、このクラスは使い物になりません。
typedef auto_handler<FILE*, int, fclose> FILE2;
{
FILE2 fp = fopen("unknown.txt", "r"); // fopen()が失敗する
} // fclose(NULL)が呼ばれてしまう。
そう、失敗時です。
これを防ぐには、生成に失敗した時は、デストラクタで後始末関数を呼ばない様にするか、コンストラクタで例外を投げてデストラクタを呼ばないようにするしかありません。
失敗したかどうかは、生成関数(上の例ではfopen())の戻り値で判断できるとしましょう(元々C言語の関数をラップするのが目的なので、成否は戻り値で行なうのが一般的)。
失敗した時は、生成関数が特定の値(fopen()ならNULL)を返すのです。
エラー時の戻り値が特定の値と決まっているのなら、
typedef auto_handler<FILE*, int, fclose, NULL> FILE3;
FILE3 fp = fopen("test.txt", "r"); // fopenに失敗したら例外を投げる
こんな風に出来ると良いですよね。
template<typename T, typename R, R (*close)(T), T invalid_value>
class auto_handler
{
auto_handler(const T& _h) : handler(_h)
{
if (handler == invalid_value)
throw runtime_error("error");
}
// (略)
};
Tが(intなどの)組み込み型の場合に限り、このテンプレートは展開可能です。
まぁ、元々は、C言語流オブジェクト指向をC++のクラスでラップするのが目的です。
C言語には参照がないので、データはポインタでやりとりされていると、強引に決め付けても、あまり差し障りがありそうもありません。FILE*も“(構造体への)ポインタ型”で、一応は組み込み型なので、OKでしょう。
ところが、gccでは
typedef auto_handler<FILE*, int, fclose, NULL> FILE3;
FILE3 fp = fopen("test.txt", "r");
これがコンパイルエラーになります。
「0をFILE*に変換できない」と言って。
ポインタ型しか扱わないと割り切ってしまえば、
template<typename T, typename R, R (*close)(T), int invalid_value>
class auto_handler
{
auto_handler(const T& _h) : handler(_h)
{
if (handler == (T)invalid_value)
throw runtime_error("error");
}
// (略)
};
キャストを使って誤魔化すことで、(大量の警告が出ますが)gccでもコンパイルできます。
しかし、これではあんまりですよね。
果たして、この問題は、解決できるのかっ!
(続く)
世のC++使いは、呆れていると思う。
template<typename T, typename Tr>
class auto_handler
{
private:
T handler;
public:
auto_handler(const T& _h) : handler(_h)
{
if (hangler == Tr::invalidate_value)
throw runtime_error("error");
}
~auto_handler() {Tr::close(handler);}
operator T&() {return handler;}
};
auto_handlerは、こうやって作っておき、使う時は、
struct file_traits
{
static const FILE* invalidate_value;
static void close(FILE* fp) {fclose(fp);}
};
const FILE* file_traits::invalidate_value = NULL;
typedef auto_handler<FILE*, file_traits> FILE4;
FILE4 fp = fopen("test.txt", "r");
おそらく、これが一番真っ当な方法じゃないかな。
(対象となるデータ型にあわせてtraitsクラスを作らなければならないけど)
どうしてもクラスを作らずに済ませたいのなら、マクロを使うというテもあります。
#define DECLARE_AUTO_HANDLER(NAME, TYPE, FUNC, VALUE) \
class NAME { \
private: \
typedef TYPE _T; \
TYPE handler; \
public: \
NAME(const _T& _h) : handler(_h) \
{ \
if (handler == VALUE) \
throw runtime_error("error"); \
} \
~NAME() {FUNC(handler);} \
operator _T&() {return handler;} \
}
DECLARE_AUTO_HANDLER(FILE5, FILE*, fclose, NULL);
FILE5 fp = fopen("test.txt", "r");
[ ▲ 先頭 ]
[update 2005/2/4]
あるクラスのオブジェクトを初期化する時に、オーバーロードされている別のコンストラクタを利用して初期化を行ないたい場合があります。
Javaだと、こうやります
public class A
{
private final int a, b;
public A()
{
this(10, 10);
}
public A(int a0, int b0)
{
a = a0;
b = b0;
}
}
これなら、aとbを使った初期化処理が増えたとしても変更箇所は1つで済みます。
例えばメンバ変数cを追加して、a×bで初期化しなければならなくなったのなら、
public class A
{
private int a, b, c;
public A()
{
this(10, 10);
}
public A(int a0, int b0)
{
a = a0;
b = b0;
c = a*b;
}
}
cの値がa+bに変更になっても修正箇所は1箇所で済みます。
同じ様な事をC++でもやりたいわけなんですよ。
で、こうやるのが一番無難な方法です。
class A
{
private:
int a, b;
public:
A(void)
{
init(10, 10);
}
A(int a0, int b0)
{
init(a0, b0);
}
private:
void init(int a0, int b0)
{
a = a0;
b = b0;
}
};
ところが、aとbがconstだった場合、これではコンパイルが通りません。
この場合、正しくはこうなります。
class A
{
private:
const int a, b;
public:
A(void) : a(10), b(10) { }
A(int a0, int b0) : a(a0), b(a0) { }
};
でも、そうすると初期化処理が2箇所に分散してしまいます。
さっきみたいにメンバ変数cを追加すると…。
class A
{
private:
const int a, b, c;
public:
A(void) : a(10), b(10), c(10*10) { }
A(int a0, int b0) : a(a0), b(b0), c(a0*b0) { }
};
コンストラクタがもっと沢山あって、更にcの初期化計算式が変更になったりでもしたら、絶対にどこかを修正し忘れる自信があります。
そこで、Javaみたいに、他のコンストラクタを呼べないものかと思ったのです。
で、こんな風に書いちゃいました。
class A
{
private:
int a, b;
public:
A(void)
{
A(10, 10); // ← コレ
}
A(int a0, int b0)
{
a = a0;
b = b0;
}
};
コンパイルもちゃんと通るし、いかにも問題無さそうに見えるんですが………。
A(10, 10)とやっている所、A(int, int)を呼び出しているんじゃなくて、コンストラクタA(int, int)を使ってAの一時オブジェクトを作っているだけじゃん(しかも、作った瞬間に破棄しているという…)。
そこで、ちょっと考えてみました。
class A
{
private:
int a, b;
public:
A(void)
{
new (this) A(10, 10);
}
A(int a0, int b0) : a(a0), b(b0) { }
};
同じ位置に同じ型のオブジェクトを作るんだから、デストラクタ周りは気にしなくても良さそうです。
………。
………。
………。
あ、ダメだ。
class C { };
class A
{
private:
int a, b;
C c;
};
こんな風に他のクラスのオブジェクトを持っていると、コンストラクタが呼ばれた時には、既にcはデフォルトコンストラクタで初期化済みだ。
そんな状態で、同じ位置にAをもう1つ作成したら、何かよくない事が起こる。
それならば、先に解放しちゃえ。
class A
{
private:
int a, b;
C c;
public:
A(void)
{
this->~A();
new (this) A(10, 10);
}
A(int a0, int b0) : a(a0), b(b0) { }
};
“Aのデストラクタが何もしない”のなら、これで良さそうです。
一番の問題は、C++の言語仕様的に、こんな事やっても大丈夫なのかどうかって事で…。
[ ▲ 先頭 ]