9

リソースを表すオブジェクトが共有ポインターに含まれている場合、リソースの割り当て解除中のエラーはどのように処理する必要がありますか?

編集1:

この質問をより具体的に言うと、多くのCスタイルのインターフェースには、リソースを割り当てる機能と、リソースを解放する機能があります。例としては、POSIXシステムのファイル記述子の場合はopen(2)とclose(2)、Xサーバーへの接続の場合はXOpenDisplayとXCloseDisplay、SQLiteデータベースへの接続の場合はsqlite3_openとsqlite3_closeがあります。

このようなインターフェイスをC++クラスにカプセル化し、Pimplイディオムを使用して実装の詳細を非表示にし、共有ポインターを返すファクトリメソッドを提供して、リソースへの参照が残っていないときにリソースの割り当てが解除されるようにします。

ただし、上記のすべての例および他の多くの例では、リソースの解放に使用された関数がエラーを報告する場合があります。この関数がデストラクタによって呼び出された場合、通常、デストラクタはスローしてはならないため、例外をスローすることはできません。

一方、リソースを解放するためのパブリックメソッドを提供する場合、2つの可能な状態を持つクラスがあります。1つはリソースが有効であり、もう1つはリソースがすでに解放されています。これはクラスの実装を複雑にするだけでなく、誤った使用法の可能性も開きます。インターフェイスは使用エラーを不可能にすることを目的としているため、これは悪いことです。

この問題について助けていただければ幸いです。

質問の元のステートメント、および可能な解決策についての考えは以下のとおりです。

編集2:

現在、この質問には報奨金があります。ソリューションは次の要件を満たす必要があります。

  1. リソースへの参照が残っていない場合にのみ、リソースが解放されます。
  2. リソースへの参照は明示的に破棄される可能性があります。リソースの解放中にエラーが発生した場合、例外がスローされます。
  3. すでにリリースされているリソースを使用することはできません。
  4. リソースの参照カウントと解放はスレッドセーフです。

ソリューション次の要件を満たす必要があります。

  1. これは、 boostC ++テクニカルレポート1(TR1)、および今後のC++標準であるC++0xによって提供される共有ポインターを使用します。
  2. それは一般的です。リソースクラスは、リソースの解放方法を実装するだけで済みます。

お手数をおかけしますが、よろしくお願いいたします。

編集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;
     }
};
4

6 に答える 6

4

一部のリソースの解放が実際に失敗する可能性がある場合、デストラクタは明らかに使用するのに間違った抽象化です。デストラクタは、状況に関係なく、必ずクリーンアップすることを目的としています。close()メソッド(または名前を付けたいもの)がおそらく唯一の方法です。

しかし、それについてよく考えてください。リソースの解放が実際に失敗した場合、何ができますか?そのようなエラーは回復可能ですか?もしそうなら、あなたのコードのどの部分がそれを処理するべきですか?回復する方法は、おそらくアプリケーション固有のものであり、アプリケーションの他の部分に関連付けられています。リソースを解放してエラーをトリガーしたコード内の任意の場所で、実際にそれが自動的に発生することを望んでいる可能性はほとんどありません。共有ポインターの抽象化は、実際に達成しようとしていることをモデル化するものではありません。もしそうなら、あなたは明らかにあなたの要求された振る舞いをモデル化するあなた自身の抽象化を作成する必要があります。共有ポインタを悪用して、想定外のことを行うのは正しい方法ではありません。

また、こちらをお読みください。

編集:
クラッシュする前に何が起こったかをユーザーに通知するだけの場合は、破棄時に削除機能を呼び出す別のラッパーオブジェクトでラップすることを検討し、スローされた例外をキャッチして、ユーザーにメッセージボックスを表示するかSocket、なんでもいい。次に、このラッパーオブジェクトを。内に配置します。boost::shared_ptr

于 2010-05-16T20:59:09.483 に答える
4

割り当てられたリソースをどこかに保存し(DeadMGですでに説明されているように)、デストラクタの外部でレポート/スロー関数を明示的に呼び出す必要があります。しかし、それは、boost::shared_ptrに実装されている参照カウントを利用することを妨げるものではありません。

/** A TCP/IP connection. */
class Socket
{
private:
    //store internally every allocated resource here
    static std::vector<boost::shared_ptr<Socket> > pool;
public:
    static boost::shared_ptr<Socket> connect(const std::string& address)
    {
         //...
         boost::shared_ptr<Socket> socket(new Socket(address));
         pool.push_back(socket); //the socket won't be actually 
                                 //destroyed until we want it to
         return socket;
    }
    virtual ~Socket();

    //call cleanupAndReport() as often as needed
    //probably, on a separate thread, or by timer 
    static void cleanupAndReport()
    {
        //find resources without clients
        foreach(boost::shared_ptr<Socket>& socket, pool)
        {
            if(socket.unique()) //there are no clients for this socket, i.e. 
                  //there are no shared_ptr's elsewhere pointing to this socket
            {
                 //try to deallocate this resource
                 if (close(socket->m_impl.fd) < 0)
                 {
                     int error = errno;
                     socket.reset(); //destroys Socket object
                     //throw an exception or handle error in-place
                     //... 
                     //throw Exception::fromErrno(error);
                 }
                 else
                 {
                     socket.reset();
                 } 
            } 
        } //foreach socket
    }
protected:
    Socket(const std::string& address);
private:
    // not implemented
    Socket(const Socket&);
    Socket& operator=(const Socket&);
};

cleanupAndReport()の実装はもう少し複雑になるはずです。現在のバージョンでは、クリーンアップ後にプールにnullポインターが入力されます。例外をスローする場合は、スローされなくなるまで関数を呼び出す必要がありますが、私は希望、それはアイデアをよく示しています。

さて、より一般的な解決策:

//forward declarations
template<class Resource>
boost::shared_ptr<Resource> make_shared_resource();
template<class Resource>
void cleanupAndReport(boost::function1<void,boost::shared_ptr<Resource> deallocator);

//for every type of used resource there will be a template instance with a static pool
template<class Resource>
class pool_holder
{
private:
        friend boost::shared_ptr<Resource> make_shared_resource<Resource>();
        friend void cleanupAndReport(boost::function1<void,boost::shared_ptr<Resource>);
        static std::vector<boost::shared_ptr<Resource> > pool;
};
template<class Resource>
std::vector<boost::shared_ptr<Resource> > pool_holder<Resource>::pool;

template<class Resource>
boost::shared_ptr<Resource> make_shared_resource()
{
        boost::shared_ptr<Resource> res(new Resource);
        pool_holder<Resource>::pool.push_back(res);
        return res;
}
template<class Resource>
void cleanupAndReport(boost::function1<void,boost::shared_ptr<Resource> > deallocator)
{
    foreach(boost::shared_ptr<Resource>& res, pool_holder<Resource>::pool)
    {
        if(res.unique()) 
        {
             deallocator(res);
        }
    } //foreach
}
//usage
        {
           boost::shared_ptr<A> a = make_shared_resource<A>();
           boost::shared_ptr<A> a2 = make_shared_resource<A>();
           boost::shared_ptr<B> b = make_shared_resource<B>();
           //...
        }
        cleanupAndReport<A>(deallocate_A);
        cleanupAndReport<B>(deallocate_B);
于 2010-05-24T21:29:27.367 に答える
1

「ExceptionalC++」の著者であるハーブサッターの引用(ここから):

デストラクタが例外をスローすると、悪いことが起こる可能性があります。具体的には、次のようなコードを検討してください。

//  The problem
//
class X {
public:
  ~X() { throw 1; }
};

void f() {
  X x;
  throw 2;
} // calls X::~X (which throws), then calls terminate()

別の例外がすでにアクティブになっているときに(つまり、スタックの巻き戻し中に)デストラクタが例外をスローすると、プログラムは終了します。これは通常、良いことではありません。

つまり、この状況でエレガントであると信じたいものに関係なく、別の例外を処理しているときにスローされないことを保証できない限り、デストラクタで例外を簡単にスローすることはできません。

また、リソースをうまく取り除くことができない場合はどうすればよいですか?バグではなく、より高い位置で処理できるものについては、例外をスローする必要があります。奇妙な動作を報告したい場合は、リリースの失敗をログに記録して、そのまま続行してください。または終了します。

于 2010-05-24T16:57:22.787 に答える
1

質問で発表されたように、3を編集します。

これは、私が判断できる限り、質問の要件を満たす別の解決策です。これは、元の質問で説明したソリューションに似ていますboost::shared_ptrが、カスタムスマートポインターの代わりに使用します。

このソリューションの中心的な考え方は、に対するrelease() 操作を提供することshared_ptrです。shared_ptr所有権を放棄できる場合は、クリーンアップ関数を呼び出し、オブジェクトを削除し、クリーンアップ中にエラーが発生した場合に例外をスローすることができます。

Boostには、次の操作を 提供しない正当な理由があります。release()shared_ptr

shared_ptrは、unique()でない限り、所有権を譲渡できません。これは、他のコピーが引き続きオブジェクトを破棄するためです。

検討:

shared_ptr<int> a(new int);
shared_ptr<int> b(a); // a.use_count() == b.use_count() == 2

int * p = a.release();

// Who owns p now? b will still call delete on it in its destructor.

さらに、release()によって返されるポインターは、ソースshared_ptrがカスタム削除機能で作成されている可能性があるため、確実に割り当てを解除するのが困難です。

操作に対する最初の議論release()は、の性質上shared_ptr、多くのポインターがオブジェクトの所有権を共有するため、それらの1つだけがその所有権を単純に解放することはできないということです。しかし、release()他の参照がまだ残っている場合に関数がnullポインターを返した場合はどうなるでしょうか?はshared_ptr、競合状態なしでこれを確実に判断できます。

操作に対する2番目の引数release()は、カスタム削除機能がに渡されたshared_ptr場合、単にオブジェクトを削除するのではなく、それを使用してオブジェクトの割り当てを解除する必要があるということです。ただしrelease() 、生のポインターに加えて関数オブジェクトを返すことで、呼び出し元がポインターの割り当てを確実に解除できるようにすることができます。

ただし、特定のシナリオでは、任意のカスタム削除者を処理する必要がないため、カスタム削除者は問題になりません。これは、以下のコードからより明確になります。

もちろん、実装を変更せずにrelease()操作を提供することは、ハックなしでは不可能です。shared_ptr以下のコードで使用されているハックは、カスタム削除者が実際にオブジェクトを削除するのを防ぐために、スレッドローカル変数に依存しています。

そうは言っても、これがコードで、主にヘッダー Resource.hppと小さな実装ファイルで構成されていますResource.cpp-lboost_thread-mtスレッドローカル変数のためにリンクする必要があることに注意してください。

// ---------------------------------------------------------------------
// Resource.hpp
// ---------------------------------------------------------------------

#include <boost/assert.hpp>
#include <boost/ref.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/thread/tss.hpp>


/// Factory for a resource.
template<typename T>
struct ResourceFactory
{
    /// Create a resource.
    static boost::shared_ptr<T>
    create()
    {
        return boost::shared_ptr<T>(new T, ResourceFactory());
    }

    template<typename A1>
    static boost::shared_ptr<T>
    create(const A1& a1)
    {
        return boost::shared_ptr<T>(new T(a1), ResourceFactory());
    }

    template<typename A1, typename A2>
    static boost::shared_ptr<T>
    create(const A1& a1, const A2& a2)
    {
        return boost::shared_ptr<T>(new T(a1, a2), ResourceFactory());
    }

    // ...

    /// Destroy a resource.
    static void destroy(boost::shared_ptr<T>& resource);

    /// Deleter for boost::shared_ptr<T>.
    void operator()(T* resource);
};


namespace impl
{

// ---------------------------------------------------------------------

/// Return the last reference to the resource, or zero. Resets the pointer.
template<typename T>
T* release(boost::shared_ptr<T>& resource);

/// Return true if the resource should be deleted (thread-local).
bool wantDelete();

// ---------------------------------------------------------------------

} // namespace impl


template<typename T>
inline
void ResourceFactory<T>::destroy(boost::shared_ptr<T>& ptr)
{
    T* resource = impl::release(ptr);

    if (resource != 0) // Is it the last reference?
    {
        try
        {
            resource->close();
        }
        catch (...)
        {
            delete resource;

            throw;
        }

        delete resource;
    }
}

// ---------------------------------------------------------------------

template<typename T>
inline
void ResourceFactory<T>::operator()(T* resource)
{
    if (impl::wantDelete())
    {
        try
        {
            resource->close();
        }
        catch (...)
        {
        }

        delete resource;
    }
}


namespace impl
{

// ---------------------------------------------------------------------

/// Flag in thread-local storage.
class Flag
{
public:
    ~Flag()
    {
        m_ptr.release();
    }

    Flag& operator=(bool value)
    {
        if (value != static_cast<bool>(*this))
        {
            if (value)
            {
                m_ptr.reset(s_true); // may throw boost::thread_resource_error!
            }
            else
            {
                m_ptr.release();
            }
        }

        return *this;
    }

    operator bool()
    {
        return m_ptr.get() == s_true;
    }

private:
    boost::thread_specific_ptr<char> m_ptr;

    static char* s_true;
};

// ---------------------------------------------------------------------

/// Flag to prevent deletion.
extern Flag t_nodelete;

// ---------------------------------------------------------------------

/// Return the last reference to the resource, or zero.
template<typename T>
T* release(boost::shared_ptr<T>& resource)
{
    try
    {
        BOOST_ASSERT(!t_nodelete);

        t_nodelete = true; // may throw boost::thread_resource_error!
    }
    catch (...)
    {
        t_nodelete = false;

        resource.reset();

        throw;
    }

    T* rv = resource.get();

    resource.reset();

    return wantDelete() ? rv : 0;
}

// ---------------------------------------------------------------------

} // namespace impl

そして実装ファイル:

// ---------------------------------------------------------------------
// Resource.cpp
// ---------------------------------------------------------------------

#include "Resource.hpp"


namespace impl
{

// ---------------------------------------------------------------------

bool wantDelete()
{
    bool rv = !t_nodelete;

    t_nodelete = false;

    return rv;
}

// ---------------------------------------------------------------------

Flag t_nodelete;

// ---------------------------------------------------------------------

char* Flag::s_true((char*)0x1);

// ---------------------------------------------------------------------

} // namespace impl

そして、このソリューションを使用して実装されたリソースクラスの例を次に示します。

// ---------------------------------------------------------------------
// example.cpp
// ---------------------------------------------------------------------
#include "Resource.hpp"

#include <cstdlib>
#include <string>
#include <stdexcept>
#include <iostream>


// uncomment to test failed resource allocation, usage, and deallocation

//#define TEST_CREAT_FAILURE
//#define TEST_USAGE_FAILURE
//#define TEST_CLOSE_FAILURE

// ---------------------------------------------------------------------

/// The low-level resource type.
struct foo { char c; };

// ---------------------------------------------------------------------

/// The low-level function to allocate the resource.
foo* foo_open()
{
#ifdef TEST_CREAT_FAILURE
    return 0;
#else
    return (foo*) std::malloc(sizeof(foo));
#endif
}

// ---------------------------------------------------------------------

/// Some low-level function using the resource.
int foo_use(foo*)
{
#ifdef TEST_USAGE_FAILURE
    return -1;
#else
    return 0;
#endif
}

// ---------------------------------------------------------------------

/// The low-level function to free the resource.
int foo_close(foo* foo)
{
    std::free(foo);
#ifdef TEST_CLOSE_FAILURE
    return -1;
#else
    return 0;
#endif
}

// ---------------------------------------------------------------------

/// The C++ wrapper around the low-level resource.
class Foo
{
public:
    void use()
    {
        if (foo_use(m_foo) < 0)
        {
            throw std::runtime_error("foo_use");
        }
    }

protected:
    Foo()
        : m_foo(foo_open())
    {
        if (m_foo == 0)
        {
            throw std::runtime_error("foo_open");
        }
    }

    void close()
    {
        if (foo_close(m_foo) < 0)
        {
            throw std::runtime_error("foo_close");
        }
    }

private:
    foo* m_foo;

    friend struct ResourceFactory<Foo>;
};

// ---------------------------------------------------------------------

typedef ResourceFactory<Foo> FooFactory;

// ---------------------------------------------------------------------

/// Main function.
int main()
{
    try
    {
        boost::shared_ptr<Foo> resource = FooFactory::create();

        resource->use();

        FooFactory::destroy(resource);
    }
    catch (const std::exception& e)
    {
        std::cerr << e.what() << std::endl;
    }

    return 0;
}

最後に、これらすべてを構築するための小さなMakefileを次に示します。

# Makefile

CXXFLAGS = -g -Wall

example: example.cpp Resource.hpp Resource.o
    $(CXX) $(CXXFLAGS) -o example example.cpp Resource.o -lboost_thread-mt

Resource.o: Resource.cpp Resource.hpp
    $(CXX) $(CXXFLAGS) -c Resource.cpp -o Resource.o

clean:
    rm -f Resource.o example
于 2010-06-16T23:26:08.670 に答える
0

さて、最初に、ここに質問はありません。第二に、これは悪い考えだと言わざるを得ません。このすべてで何が得られますか?リソースへの最後の共有ポインタが破棄され、スローする削除プログラムが呼び出されると、リソースリークが発生します。リリースに失敗したリソースへのすべてのハンドルが失われます。再試行することはできなくなります。

RAIIオブジェクトを使用したいというあなたの願望は良いものですが、スマートポインターは単にタスクには不十分です。必要なものはさらに賢くする必要があります。完全に崩壊しなかったときに自分自身を再構築できるものが必要です。デストラクタは、このようなインターフェイスには不十分です。

あなたは、誰かがリソースにハンドルを持たせても無効にする可能性があるという誤用を自己紹介します。ここで扱っているリソースの種類は、単にこの問題に役立ちます。これにアプローチする方法はたくさんあります。1つの方法は、状態パターンとともにハンドル/ボディのイディオムを使用することです。インターフェイスの実装は、接続または非接続の2つの状態のいずれかになります。ハンドルは、リクエストを内部の本体/状態に渡すだけです。Connectedは通常のように機能し、unconnectedは該当するすべてのリクエストで例外/アサートをスローします。

これには、ハンドルを破棄するために〜以外の関数が必要になります。スローできるdestroy()関数を検討できます。呼び出したときにエラーが発生した場合は、ハンドルを削除せずに、アプリケーション固有の方法で問題に対処します。destroy()でエラーが発生しない場合は、ハンドルをスコープ外にするか、リセットするか、その他何でもします。次に、関数destroy()は、リソースカウントをデクリメントし、そのカウントが0の場合、内部リソースの解放を試みます。成功すると、ハンドルは非接続状態に切り替わり、失敗すると、クライアントが処理を試みることができるが、そのままにしておくキャッチ可能なエラーを生成します。接続状態のハンドル。

書くことは完全に些細なことではありませんが、あなたがやりたいこと、破壊に例外を導入することは、単に機能しません。

于 2010-05-16T20:04:59.063 に答える
0

一般的に、リソースのCスタイルのクロージャーが失敗した場合、それはコードの問題ではなく、APIの問題です。ただし、破棄に失敗した場合は、後でアプリが終了したとき、定期的に、または他の同様のリソースが破棄されたときに、破棄/クリーンアップを再試行する必要があるリソースのリストに追加します。その後、再破壊してみてください。任意の時間に残っているものがある場合は、ユーザーエラーを出して終了します。

于 2010-05-21T20:03:45.347 に答える