C ++ 11のRAIIおよび委任コンストラクター

この投稿では、コンストラクターの委任と呼ばれるC ++ 11の興味深い機能について説明します。なぜ興味深いのか、より効率的なリソース管理に適用できる方法、つまり イディオムRAIIの実装。







RAIIについて簡単に(まあ、非常に簡単に)


いくつかの「裸の」リソースの管理を自動化する必要がある場合、それを別のクラスに「ラップ」します。 C標準ライブラリのFILEなどのリソースの例を使用して、これを示しましょう。



#include <stdio.h> class File { public: File(char const * filename, char const * mode) : file_(fopen(filename, mode)) {} ~File() { fclose(file_); } File(File const &) = delete; File operator=(File const &) = delete; // file operations // ... private: FILE * file_; };
      
      







ここでは、コンストラクタでFILEリソースを作成し、デストラクタで解放します。 FILEリソースは、RAIIイディオムに完全に準拠して管理されるようになりました。



RAIIのやや複雑なケース


ここで、コンストラクターでファイルを開くことに加えて、そのファイルで何らかの操作を実行する必要があるとします。 たとえば、新しく開いたファイルに最後に開いた時間、タイムスタンプを記録します。 これを行うには、Fileクラスにput_time_stamp関数を作成します。この関数は、何らかの方法でファイルにタイムスタンプを配置し、失敗した場合は例外をスローします。



このケースは次のように実装されます。



 #include <stdio.h> class File { public: File(char const * filename, char const * mode) : file_(fopen(filename, mode)) { put_time_stamp(); } ~File() { fclose(file_); } File(File const &) = delete; File operator=(File const &) = delete; // file operations void put_time_stamp() { // throws on error // ... } private: FILE * file_; };
      
      







しかし、ご覧のとおり、この実装には小さな問題があります。 Fileコンストラクターは例外安全ではなくなりました。 put_time_stampから例外がスローされた場合、コンストラクタはまだ完了していないため、Fileオブジェクトのデストラクターの呼び出しは発生しません。 したがって、file_リソースは失われます。



この問題をどのように解決しますか? 率直な解決策は、put / time_stamp呼び出しをtry / catchブロックでラップすることです。



 class File { public: File(char const * filename, char const * mode) try : file_(fopen(filename, mode)) { put_time_stamp(); } catch (...) { destruct_obj(); } ~File() { destruct_obj(); } private: void destruct_obj() { fclose(file_); } FILE * file_; };
      
      







このアプローチは機能しますが、catchブロックとデストラクタで同じ機能を複製しないように、明示的なtry / catchブロックとオブジェクトを明示的に破棄する別のメソッドが必要なため、少しいです。



FILE、FileHandleの保存と削除に特化した追加のクラスを導入すると、このソリューションを少し改善できます。



 class File { struct FileHandle { FileHandle(FILE * fh) : fh_(fh) {} ~FileHandle() { fclose(fh_); } FILE * fh_; } public: File(char const * filename, char const * mode) : file_(fopen(filename, mode)) { put_time_stamp(); } ~File() = default; private: FileHandle file_; };
      
      







ご覧のとおり、明示的なtry / catchブロックは不要になりました。 Fileクラスのコンストラクターから例外がスローされ、FILEリソースが解放された場合でも、file_オブジェクトは正しく破棄されます。 しかし、このソリューションにはまだ欠点があります。このソリューションは、FILEリソースの作成とリリースを2つの異なるクラスに分割する別個のFileHandleクラスで構成されます。FILEはFileクラスで作成され、FileHandleクラスで解放されます。



コンストラクターの委任


次に、委任コンストラクターと呼ばれるC ++ 11の非常に便利な機能を見てみましょう。これにより、Fileクラスの以前のコードをさらに改善できます。 しかし、まず、これらの委任コンストラクターが一般的にどのように機能するかを見てみましょう。



2つのコンストラクターを持つクラスがあるとします。1つはint型のパラメーターから、もう1つはdoubleからです。 intのコンストラクターは、doubleのコンストラクターと同じことを行いますが、最初はパラメーターをintからdoubleに変換します。 つまり intのコンストラクターは、オブジェクトの作成をdoubleのコンストラクターに委任します。 コードでは次のようになります。



 class MyClass { public: MyClass(double param) { // construct object for double parameter } MyClass(int param) : MyClass(double(param)) // call ctor for double { // do some additional operations for int parameter // if necessary } };
      
      







doubleのコンストラクターの実行が終了した後、intのコンストラクターは引き続き実行してオブジェクトを「再構築」できます。 これ自体は非常に便利な機能であり、上記のコードではおそらく、追加のinit(double param)関数を導入して、double型のオブジェクトを作成するための一般的なコードをカプセル化する必要があります。



しかし、さらに、この機能には非常に興味深い副作用が1つあります。 実際、オブジェクトのコンストラクターの1つが実行を終了するとすぐに、オブジェクトは作成されたと見なされます。 つまり、最初のコンストラクターの委任呼び出しが発生した別のコンストラクターが例外がスローされて終了した場合、このオブジェクトに対してデストラクタが呼び出されます。 重要な点に注意してください。オブジェクトに対して複数のコンストラクターを実行できるようになりました。 ただし、オブジェクトは最初のコンストラクターの後に作成されたと見なされます。



この動作を次の例で示します。



 class MyClass { public: MyClass(double) { cout << "ctor(double)\n"; } MyClass(int val) : MyClass(double(val)) { cout << "ctor(int)\n"; throw "oops!"; } ~MyClass() { cout << "dtor\n"; } }; int main() try { MyClass obj(10); cout << "obj created"; } catch (...) { cout << "exception\n"; }
      
      







コンストラクターMyClass(int)は、別のコンストラクターMyClass(double)を呼び出し、その後例外をスローします。 この例外はcatch(...)でキャッチされ、スタックが解けると〜MyClassデストラクタが呼び出されます。 このコードを実行すると、以下がコンソールに表示されます:



 ctor(double) ctor(int) dtor exception
      
      







コンストラクターとRAIIの委任


委任中のコンストラクタのこのような興味深い動作は、RAII for FILEの実装例で非常に効果的に使用できることに気付くのは簡単です。 FILEリソースを解放するために追加のFileHandleクラスを導入する必要はありません。try/ catchを大幅に削減しました。 メインコンストラクターから委任される追加のコンストラクターを1つだけ入力する必要があります。 それは:



 class File { File(FILE * file) : file_(file) {} public: File(char const * filename, char const * mode) : File(fopen(filename, mode)) { put_time_stamp(); } ~File() { fclose(file_); } void put_time_stamp() { ... } private: FILE * file_; };
      
      







そして、それは私たちが必要とするすべてです。 非常に美しく、エレガントで、例外に対して完全に安全です(例外安全)。 結論:同様の手法により、C ++ 11の委任コンストラクターを使用して、新しいコードでRAIIイディオムの実装が大幅に促進されます。



All Articles