ストリームオブジェクトを、単純にコピーしたり割り当てることはできません。これを実際の例で検討します。プログラムの呼び出しで、ファイル名が指定されている場合はファイルに、ファイル名が指定されていない場合は標準出力ストリーム cout に、プログラムはデータを書き込みます。プログラムでは 1 つの出力ストリームに書き込むことができます。このストリームはファイルストリームの場合と、標準出力ストリームの場合があります。一般には、出力ファイルストリームオブジェクトを宣言し、それを cout に割り当てるか、直接的に cout を使用します。ただし、次の方法は不可能です。
int main(int argc, char argv[]) { ofstream fil; if (argc > 1) { fil.open(argv[1]); cout = fil; // これは行わないでください !!! } // cout などへの出力 cout << "Hello world!" << endl; }
この解法は、少なくとも次の 2 点で無効です。1 つは、禁止されている定義済みストリームに対する割り当てが使用されていることです。定義済みのストリーム cin、cout、cerr、clog には専用の特性があり、他のストリームとは処理方法が異なります。上記の例の cout の場合のように新たな割り当てを行うと、専用の特性が失われます。 もう 1 つは、ストリームの割り当てとコピーには危険性があるということです。出力ストリーム fil を割り当てた場合、コンパイルをすることができ、また、そのまま使用することもできるかもしれません。しかし、プログラムがいずれクラッシュする可能性があります。17
注: ストリームオブジェクト相互のコピーや割り当ては行わないでください。
その理由を考えてみましょう。ストリームのコピーコンストラクタと、割り当て演算子は定義されていません。18 そのため、コンパイラで生成されるデフォルトのコピーコンストラクタと割り当て演算子が使用されます。他の場合と同様に、コピーや割り当ての対象は、ストリームオブジェクトのデータメンバーだけです。しかし、ストリームオブジェクトにはポインタも組み込まれているため、これでは不完全です。図 30 は、ストリームオブジェクトに組み込まれるデータを表したものです。
ストリームバッファには、実際に使用する文字バッファに対する複数のポインタが設定されています。デフォルトコピーコンストラクタと割り当て演算子では、これらのポインタを正しく処理することはできません。
データメンバーを 1 つずつコピーすると、コピーと同じ結果を得ることができます。次にその方法を示します。
int main(int argc, char argv[]) { ofstream out; if (argc > 1) out.open(argv[1]); else { out.copyfmt(cout); //1 out.clear(cout.rdstate()); //2 out.rdbuf(cout.rdbuf()); //3 } // out などに対する出力 out << "Hello world!" << endl; }
//1 | copyfmt() 関数では、エラー状態とストリームバッファを除いて、標準出力ストリーム cout からのすべてのデータが出力ファイルストリーム out にコピーされます。 cout.exceptions(fil.exceptions()) のように、例外マスクを別途にコピーすることのできる関数 exceptions() もありますが、copyfmt() で例外マスクがコピーされるため、これを明示的に実行する必要はありません。 |
//2 | ここで、エラー状態がコピーされます。 |
//3 | ここで、ストリームバッファポインタがコピーされます。 |
ここで、小さな障害があることに注意してください。rdbuf() に対する呼び出しの後に、図 31 のようにバッファは 2 つのストリームによって共有されます。
ストリームでストリームバッファを共有するかどうかは、アプリケーションによって異なります。いずれの場合でも、rdbuf() を呼び出すと、ストリームバッファが共有されるということを認識することが重要です。つまり、ストリームバッファオブジェクトの寿命を監視し、必ずストリームの寿命より長くする必要があります。上記の簡単な例では、標準出力ストリームのバッファを使用しています。標準ストリームは静的オブジェクトであり、そのストリームバッファの寿命は、他のほとんどのオブジェクトより長くなっているため、寿命の点では問題ありません。しかし、他のストリームオブジェクトとストリームバッファを共有する場合は、ストリームバッファの寿命に十分注意してください。
今までは考慮していませんでしたが、次のコードのように、上記の例にはもう 1 つの問題があります。
int main(int argc, char argv[]) { ofstream out; if (argc > 1) out.open(argv[1]); else { out.copyfmt(cout); //1 out.clear(cout.rdstate()); //2 out.rdbuf(cout.rdbuf()); //3 } // out などに対する出力。 out << "Hello world!" << endl; }
//1 | cout のメンバー変数 (ストリームバッファと入出力状態以外) を out にコピーします。 |
//2 | out の状態フラグを cout の現在の状態に設定します。 |
//3 | out のストリームバッファを cout のストリームバッファに置き換えます。 |
標準出力ストリームの内部データ全体をコピーするとき、その特別な動作もコピーします。たとえば、標準出力ストリームは、標準入力ストリームと同期します (詳細は、第 14 章参照)。出力ファイルストリーム out が cout のコピーの場合、その出力演算と、cin のすべての入力演算が強制的に同期します。同期には時間がかかるため、これは望ましいことではありません。そこで、標準出力ストリームのストリームバッファだけを使用した、より効率的な方法を次に示します。
int main(int argc, char argv[]) { filebuf* fb = new filebuf; //1 ostream out((argc>1)? //2 fb->open(argv[1],ios_base::out|ios_base::trunc): //3 cout.rdbuf()); //4 if (out.rdbuf() != fb) delete fb; out << "Hello world!" << endl; }
//1 | ファイルバッファオブジェクトをすでに含むファイルストリームオブジェクトを作成する代わりに、独立したファイルバッファオブジェクトをヒープに作成します。これは、必要に応じて、出力ストリームオブジェクトに渡すことができるオブジェクトです。これにより、必要がない場合は、ファイルバッファオブジェクトを削除することができます。元の例で示したファイルストリームオブジェクトの構築では、ファイルバッファオブジェクトを使用していなくても削除する機会がありませんでした。 |
//2 | 出力ストリームを作成します。このストリームには、標準出力ストリームのバッファか、ファイルに結合されたファイルバッファがあります。 |
//3 | プログラムにファイル名を指定すると、ファイルが開いてファイルバッファオブジェクトに結合します (このストリームバッファオブジェクトの寿命は、使用する出力ストリームよりも長くしてください)。 open() 関数は、ファイルバッファオブジェクトまでのポインタを返します。このポインタは出力ストリームオブジェクトの作成に使用します。 |
//4 | ファイル名を指定しない場合は、標準出力ストリームのバッファが使用されます。 |
元の例では、out は標準出力ストリームのバッファで挿入しますが、標準ストリームの特別な特性が欠けています。
これに対して、次のコードでは、ファイル記述子を使用しており、これは、標準入出力を Rogue Wave で実装した特別な機能です。19
int main(int argc, char argv[]) { ofstream out; if (argc > 1) out.open(argv[1]); //1 else out.rdbuf()->open(1); //2 out << "Hello world!" << endl; }
//1 | このプログラムにファイル名を与えると、ファイルが開いてファイルバッファオブジェクトに結合されます。 |
//2 | ファイル名を省略すると、出力ストリームのファイルバッファは、ファイル記述子が 1 の標準入力ストリーム stdout に結合します。 |
標準出力ストリーム cout は、C 標準入力ファイル stdout に結合するため、結果は前述の方法と同じです。ストリームバッファの再割り当てや共有がないため、この解法が最も簡潔です。その場合でも、出力ファイルストリームのバッファは正しいファイルと結合します。しかし、この解法は一般的ではなく、移植性が損なわれる場合があります。
ストリームバッファをまったく使用しない場合は、ストリームまでのポインタや参照を使用します。次に例を示します。
int main(int argc, char argv[]) { ostream* fp; //1 if (argc > 1) fp = new ofstream(argv[1]); //2 else fp = &cout //3 // *fp などに対する出力。 *fp << "Hello world!" << endl; //4 if(fp!=&cout) delete fp; }
//1 | ostream までのポインタを使用します (ofstream までのポインタは使用できません。標準出力ストリーム cout はファイルストリームではなく、ostream 型の単純なストリームだからです)。 |
//2 | ファイル名を指定すれば、指定した出力ファイルのファイルストリームがヒープに作成され、ポインタに割り当てられます。 |
//3 | ファイル名を省略すると、cout までのポインタが使用されます。 |
//4 | cout か指定した出力ファイルまでのポインタで、出力が書き込まれます。 |
次に、ポインタの代わりに参照を使用する方法を示します。
int main(int argc, char argv[]) { ostream& fr = (argc > 1) ? *(new ofstream(argv[1])) : cout; // *fr などに対する出力。 fr << "Hello world!" << endl; if (&fr!=&cout) delete(&fr); }
ポインタと参照を使用する方法には、欠点もあります。ヒープ上で出力ファイルストリームオブジェクトを作成する必要があり、オブジェクトが削除される可能性があるからです。
結局、ストリームのコピーを作成することはそれほど簡単ではないため、ストリームオブジェクトのコピーが確実に必要な場合だけに行うのが賢明です。ほとんどの場合、ストリームオブジェクトまでの参照やポインタを使用するか、2 つのストリーム間でストリームバッファを共有する方法で対応することができます。
注: ストリームオブジェクトまでの参照やポインタで対応できる場合や、共有ストリームバッファで問題に対応できる場合は、ストリームオブジェクトのコピーは作成しないでください。
Copyright (c) 1998, Rogue Wave Software, Inc.
このマニュアルに関する誤りのご指摘やご質問は、電子メールにてお送りください。
OEM リリース, 1998 年 6 月