リソースを表すオブジェクトが共有ポインターに含まれている場合、リソースの割り当て解除中のエラーはどのように処理する必要がありますか?
編集1:
この質問をより具体的に言うと、多くのCスタイルのインターフェースには、リソースを割り当てる機能と、リソースを解放する機能があります。例としては、POSIXシステムのファイル記述子の場合はopen(2)とclose(2)、Xサーバーへの接続の場合はXOpenDisplayとXCloseDisplay、SQLiteデータベースへの接続の場合はsqlite3_openとsqlite3_closeがあります。
このようなインターフェイスをC++クラスにカプセル化し、Pimplイディオムを使用して実装の詳細を非表示にし、共有ポインターを返すファクトリメソッドを提供して、リソースへの参照が残っていないときにリソースの割り当てが解除されるようにします。
ただし、上記のすべての例および他の多くの例では、リソースの解放に使用された関数がエラーを報告する場合があります。この関数がデストラクタによって呼び出された場合、通常、デストラクタはスローしてはならないため、例外をスローすることはできません。
一方、リソースを解放するためのパブリックメソッドを提供する場合、2つの可能な状態を持つクラスがあります。1つはリソースが有効であり、もう1つはリソースがすでに解放されています。これはクラスの実装を複雑にするだけでなく、誤った使用法の可能性も開きます。インターフェイスは使用エラーを不可能にすることを目的としているため、これは悪いことです。
この問題について助けていただければ幸いです。
質問の元のステートメント、および可能な解決策についての考えは以下のとおりです。
編集2:
現在、この質問には報奨金があります。ソリューションは次の要件を満たす必要があります。
- リソースへの参照が残っていない場合にのみ、リソースが解放されます。
- リソースへの参照は明示的に破棄される可能性があります。リソースの解放中にエラーが発生した場合、例外がスローされます。
- すでにリリースされているリソースを使用することはできません。
- リソースの参照カウントと解放はスレッドセーフです。
ソリューションは次の要件を満たす必要があります。
- これは、 boost、C ++テクニカルレポート1(TR1)、および今後のC++標準であるC++0xによって提供される共有ポインターを使用します。
- それは一般的です。リソースクラスは、リソースの解放方法を実装するだけで済みます。
お手数をおかけしますが、よろしくお願いいたします。
編集3:
私の質問に答えてくれたみんなに感謝します。
アルスクの答えは、賞金で要求されたすべてを満たし、受け入れられました。マルチスレッドコードでは、このソリューションには個別のクリーンアップスレッドが必要になります。
別のクリーンアップスレッドを必要とせずに、クリーンアップ中の例外が実際にリソースを使用したスレッドによってスローされるという別の回答を追加しました。あなたがまだこの問題に興味を持っているなら(それは私をとても悩ませました)、コメントしてください。
スマートポインタは、リソースを安全に管理するための便利なツールです。このようなリソースの例としては、メモリ、ディスクファイル、データベース接続、またはネットワーク接続があります。
// open a connection to the local HTTP port
boost::shared_ptr<Socket> socket = Socket::connect("localhost:80");
典型的なシナリオでは、リソースをカプセル化するクラスはコピー不可能でポリモーフィックである必要があります。これをサポートする良い方法は、共有ポインターを返すファクトリメソッドを提供し、すべてのコンストラクターを非公開として宣言することです。これで、共有ポインタを自由にコピーして割り当てることができます。オブジェクトへの参照がなくなると、オブジェクトは自動的に破棄され、デストラクタはリソースを解放します。
/** A TCP/IP connection. */
class Socket
{
public:
static boost::shared_ptr<Socket> connect(const std::string& address);
virtual ~Socket();
protected:
Socket(const std::string& address);
private:
// not implemented
Socket(const Socket&);
Socket& operator=(const Socket&);
};
しかし、このアプローチには問題があります。デストラクタはスローしてはならないため、リソースの解放の失敗は検出されないままになります。
この問題を解決する一般的な方法は、リソースを解放するためのパブリックメソッドを追加することです。
class Socket
{
public:
virtual void close(); // may throw
// ...
};
残念ながら、このアプローチでは別の問題が発生します。オブジェクトには、すでにリリースされているリソースが含まれている可能性があります。これにより、リソースクラスの実装が複雑になります。さらに悪いことに、クラスのクライアントがそれを誤って使用する可能性があります。次の例はとてつもないように見えるかもしれませんが、マルチスレッドコードの一般的な落とし穴です。
socket->close();
// ...
size_t nread = socket->read(&buffer[0], buffer.size()); // wrong use!
オブジェクトが破棄される前にリソースが解放されないようにすることで、失敗したリソースの割り当て解除に対処する方法が失われます。または、オブジェクトの存続期間中にリソースを明示的に解放する方法を提供します。これにより、リソースクラスを誤って使用できるようになります。
このジレンマから抜け出す方法があります。ただし、解決策には、変更された共有ポインタークラスを使用することが含まれます。これらの変更は物議を醸す可能性があります。
boost :: shared_ptrなどの一般的な共有ポインタの実装では、オブジェクトのデストラクタが呼び出されたときに例外がスローされないようにする必要があります。一般に、デストラクタはスローしてはならないため、これは妥当な要件です。これらの実装では、オブジェクトへの参照が残っていない場合にデストラクタの代わりに呼び出されるカスタム削除関数を指定することもできます。スローなしの要件は、このカスタム削除機能に拡張されています。
この要件の論理的根拠は明らかです。共有ポインタのデストラクタはスローしてはなりません。削除関数がスローしない場合、または共有ポインターのデストラクタもスローしません。ただし、同じことが、リソースの割り当て解除につながる共有ポインターの他のメンバー関数にも当てはまります。たとえば、reset():リソースの割り当て解除が失敗した場合、例外をスローすることはできません。
ここで提案する解決策は、カスタム削除関数をスローできるようにすることです。これは、変更された共有ポインタのデストラクタが、deleter関数によってスローされた例外をキャッチする必要があることを意味します。一方、デストラクタ以外のメンバー関数(reset()など)は、デリータ関数の例外をキャッチしてはなりません(その実装はやや複雑になります)。
スロー削除関数を使用した元の例を次に示します。
/** A TCP/IP connection. */
class Socket
{
public:
static SharedPtr<Socket> connect(const std::string& address);
protected:
Socket(const std::string& address);
virtual Socket() { }
private:
struct Deleter;
// not implemented
Socket(const Socket&);
Socket& operator=(const Socket&);
};
struct Socket::Deleter
{
void operator()(Socket* socket)
{
// Close the connection. If an error occurs, delete the socket
// and throw an exception.
delete socket;
}
};
SharedPtr<Socket> Socket::connect(const std::string& address)
{
return SharedPtr<Socket>(new Socket(address), Deleter());
}
これで、reset()を使用してリソースを明示的に解放できます。別のスレッドまたはプログラムの別の部分にリソースへの参照がまだある場合、reset()を呼び出すと、参照カウントがデクリメントされるだけです。これがリソースへの最後の参照である場合、リソースは解放されます。リソースの割り当て解除が失敗すると、例外がスローされます。
SharedPtr<Socket> socket = Socket::connect("localhost:80");
// ...
socket.reset();
編集:
削除機能の完全な(ただしプラットフォームに依存する)実装は次のとおりです。
struct Socket::Deleter
{
void operator()(Socket* socket)
{
if (close(socket->m_impl.fd) < 0)
{
int error = errno;
delete socket;
throw Exception::fromErrno(error);
}
delete socket;
}
};