2

ビルドにかなりの時間がかかる可能性のあるプロジェクトを維持しているため、可能な限り依存関係を削減しようとしています。一部のクラスは、pImplイディオムと私がこれを正しく行うこと、およびクラスが STL (特にコンテナー) とうまく連携することを確認したい場合に使用できます。 ? std::auto_ptr実装ポインタに使用しています - これは許容できますか? を使用するboost::shared_ptr方が良いでしょうか?

andSampleImplという名前のクラスを使用するクラスのコードを次に示します。FooBar

// SampleImpl.h
#ifndef SAMPLEIMPL_H
#define SAMPLEIMPL_H

#include <memory>

// Forward references
class Foo;
class Bar;

class SampleImpl
{
public:
    // Default constructor
    SampleImpl();
    // Full constructor
    SampleImpl(const Foo& foo, const Bar& bar);
    // Copy constructor
    SampleImpl(const SampleImpl& SampleImpl);
    // Required for std::auto_ptr?
    ~SampleImpl();
    // Assignment operator
    SampleImpl& operator=(const SampleImpl& rhs);
    // Equality operator
    bool operator==(const SampleImpl& rhs) const;
    // Inequality operator
    bool operator!=(const SampleImpl& rhs) const;

    // Accessors
    Foo foo() const;
    Bar bar() const;

private:
    // Implementation forward reference
    struct Impl;
    // Implementation ptr
    std::auto_ptr<Impl> impl_;
};

#endif // SAMPLEIMPL_H

// SampleImpl.cpp
#include "SampleImpl.h"
#include "Foo.h"
#include "Bar.h"

// Implementation definition
struct SampleImpl::Impl
{
    Foo foo_;
    Bar bar_;

    // Default constructor
    Impl()
    {
    }

    // Full constructor
    Impl(const Foo& foo, const Bar& bar) :
        foo_(foo),
        bar_(bar)
    {
    }
};

SampleImpl::SampleImpl() :
    impl_(new Impl)
{
}

SampleImpl::SampleImpl(const Foo& foo, const Bar& bar) :
    impl_(new Impl(foo, bar))
{
}

SampleImpl::SampleImpl(const SampleImpl& sample) :
    impl_(new Impl(*sample.impl_))
{
}

SampleImpl& SampleImpl::operator=(const SampleImpl& rhs)
{
    if (this != &rhs)
    {
        *impl_ = *rhs.impl_;
    }
    return *this;
}

bool SampleImpl::operator==(const SampleImpl& rhs) const
{
    return  impl_->foo_ == rhs.impl_->foo_ &&
        impl_->bar_ == rhs.impl_->bar_;
}

bool SampleImpl::operator!=(const SampleImpl& rhs) const
{
    return !(*this == rhs);
}

SampleImpl::~SampleImpl()
{
}

Foo SampleImpl::foo() const
{
    return impl_->foo_;
}

Bar SampleImpl::bar() const
{
    return impl_->bar_;
}
4

3 に答える 3

3

コピー中に Foo または Bar がスローされる可能性がある場合は、代入にコピー アンド スワップの使用を検討する必要があります。これらのクラスの定義を見ないと、できるかどうかはわかりません。彼らの公開されたインターフェースを見なければ、あなたが気付かないうちに、彼らが将来そうするように変わるかどうかを言うことはできません.

jalf が言うように、auto_ptr の使用は少し危険です。コピーや代入で思い通りに動作しません。ざっと見てみると、コードでは impl_ メンバーのコピーまたは割り当てが許可されていないと思うので、おそらく問題ありません。

ただし、scoped_ptr を使用できる場合、コンパイラは、誤って変更されていないことを確認するというトリッキーな仕事をしてくれます。const魅力的かもしれませんが、交換することはできません。

于 2010-01-08T14:47:19.770 に答える
2

Pimpl にはいくつかの問題があります。

まず第一に、明らかではありませんが: Pimpl を使用する場合は、コピー コンストラクター/代入演算子とデストラクター (現在は "Dreaded 3" として知られています) を定義する必要があります。

適切なセマンティックを持つ適切なテンプレート クラスを作成することで、これを簡単に行うことができます。

問題は、前方宣言を使用したためにコンパイラが「Dreaded 3」のいずれかを定義するように設定した場合、前方宣言されたオブジェクトの「Dreaded 3」を呼び出す方法を知っていることです...

最も驚くべきこと: ほとんどstd::auto_ptrの場合は動作するように見えますが、delete動作しないため予期しないメモリ リークが発生します。ただし、カスタム テンプレート クラスを使用すると、コンパイラは必要な演算子が見つからないと文句を言うでしょう (少なくとも、gcc 3.4.2 での私の経験ではそうです)。

おまけとして、私自身のにきびクラス:

template <class T>
class pimpl
{
public:
  /**
   * Types
   */
  typedef const T const_value;
  typedef T* pointer;
  typedef const T* const_pointer;
  typedef T& reference;
  typedef const T& const_reference;

  /**
   * Gang of Four
   */
  pimpl() : m_value(new T) {}
  explicit pimpl(const_reference v) : m_value(new T(v)) {}

  pimpl(const pimpl& rhs) : m_value(new T(*(rhs.m_value))) {}

  pimpl& operator=(const pimpl& rhs)
  {
    pimpl tmp(rhs);
    swap(tmp);
    return *this;
  } // operator=

  ~pimpl() { delete m_value; }

  void swap(pimpl& rhs)
  {
    pointer temp(rhs.m_value);
    rhs.m_value = m_value;
    m_value = temp;
  } // swap

  /**
   * Data access
   */
  pointer get() { return m_value; }
  const_pointer get() const { return m_value; }

  reference operator*() { return *m_value; }
  const_reference operator*() const { return *m_value; }

  pointer operator->() { return m_value; }
  const_pointer operator->() const { return m_value; }

private:
  pointer m_value;
}; // class pimpl<T>

// Swap
template <class T>
void swap(pimpl<T>& lhs, pimpl<T>& rhs) { lhs.swap(rhs); }

ブースト (特にキャストの問題) はあまり考慮されていませんが、いくつかの優れた点があります。

  • 適切なコピー セマンティック (深い)
  • 適切な const の伝播

あなたはまだ「ドレッド3」を書かなければなりません。しかし、少なくとも値セマンティックで扱うことができます。


EDIT : Frerich Raabe に刺激されて、これが怠惰なバージョンです。Big Three (現在は Four) を書くのが面倒な場合です。

アイデアは、完全な型が利用可能な情報を「キャプチャ」し、抽象インターフェイスを使用して操作可能にすることです。

struct Holder {
    virtual ~Holder() {}
    virtual Holder* clone() const = 0;
};

template <typename T>
struct HolderT: Holder {
    HolderT(): _value() {}
    HolderT(T const& t): _value(t) {}

    virtual HolderT* clone() const { return new HolderT(*this); }
    T _value;
};

そして、これを使用して、真のコンパイル ファイアウォール:

template <typename T>
class pimpl {
public:
    /// Types
    typedef T value;
    typedef T const const_value;
    typedef T* pointer;
    typedef T const* const_pointer;
    typedef T& reference;
    typedef T const& const_reference;

    /// Gang of Five (and swap)
    pimpl(): _holder(new HolderT<T>()), _p(this->from_holder()) {}

    pimpl(const_reference t): _holder(new HolderT<T>(t)), _p(this->from_holder()) {}

    pimpl(pimpl const& other): _holder(other->_holder->clone()),
                               _p(this->from_holder())
    {}

    pimpl(pimpl&& other) = default;

    pimpl& operator=(pimpl t) { this->swap(t); return *this; }

    ~pimpl() = default;

    void swap(pimpl& other) {
        using std::swap;
        swap(_holder, other._holder);
        swap(_p, other._p)
    }

    /// Accessors
    pointer get() { return _p; }
    const_pointer get() const { return _p; }

    reference operator*() { return *_p; }
    const_reference operator*() const { return *_p; }

    pointer operator->() { return _p; }
    const_pointer operator->() const { return _p; }

private:
    T* from_holder() { return &static_cast< HolderT<T>& >(*_holder)._value; }

    std::unique_ptr<Holder> _holder;
    T* _p;           // local cache, not strictly necessary but avoids indirections
}; // class pimpl<T>

template <typename T>
void swap(pimpl<T>& left, pimpl<T>& right) { left.swap(right); }
于 2010-01-08T17:22:16.077 に答える
0

私は同じ質問に苦労してきました。答えは次のとおりです。

賢明なことをするためにコピー演算子と代入演算子を定義する限り、あなたはあなたが提案していることをすることができます。

STLコンテナが物のコピーを作成することを理解することが重要です。それで:

class Sample {
public:
    Sample() : m_Int(5) {}
    void Incr() { m_Int++; }
    void Print() { std::cout << m_Int << std::endl; }
private:
    int m_Int;
};

std::vector<Sample> v;
Sample c;
v.push_back(c);
c.Incr();
c.Print();
v[0].Print();

これからの出力は次のとおりです。

6
5

つまり、ベクトルはc自体ではなく、cのコピーを格納しています。

したがって、PIMPLクラスとして書き直すと、次のようになります。

class SampleImpl {
public:
    SampleImpl() : pimpl(new Impl()) {}
    void Incr() { pimpl->m_Int++; }
    void Print() { std::cout << m_Int << std::endl; }
private:
    struct Impl {
        int m_Int;
        Impl() : m_Int(5) {}
    };
    std::auto_ptr<Impl> pimpl;
};

簡潔にするために、PIMPLイディオムを少し混乱させていることに注意してください。これをベクターにプッシュしようとしても、SampleImplクラスのコピーを作成しようとします。ただし、これは機能しません。これは、std::vector格納するものが、コピーするものを変更しないコピーコンストラクターを提供する必要があるためです。

正確に1つが所有するauto_ptrものを指しauto_ptrます。では、のコピーを作成するときauto_ptr、基になるポインタを所有しているのはどれですか?古いauto_ptrものか新しいものか?基になるオブジェクトのクリーンアップを担当するのはどれですか?答えは、所有権がコピーに移動し、オリジナルがへのポインタとして残されるということnullptrです。

ベクターでの使用を妨げる欠落しているのは、コピーされるものauto_ptrへのconst参照を取得するコピーコンストラクターです。

auto_ptr<T>(const auto_ptr<T>& other);

(または同様のもの-すべてのテンプレートパラメータを覚えているわけではありません)。これを提供し、最初の例の関数で上記のクラスauto_ptrを使用しようとすると、クラッシュします。これは、ベクターにプッシュすると、の所有権がベクター内のオブジェクトに譲渡され、所有権がなくなるためです。 。したがって、を呼び出すと、プロセスは逆参照のセグメンテーション違反でクラッシュします。SampleImplmain()cauto_ptrpimplcc.Incr()nullptr

したがって、クラスの基礎となるセマンティクスを決定する必要があります。それでも「すべてをコピーする」動作が必要な場合は、それを正しく実装するコピーコンストラクターを提供する必要があります。

    SampleImpl(const SampleImpl& other) : pimpl(new Impl(*(other.pimpl))) {}
    SampleImpl& operator=(const SampleImpl& other) { pimpl.reset(new Impl(*(other.pimpl))); return *this; }

これで、SampleImplのコピーを取得しようとすると、コピーSampleImplが所有するImpl構造体のコピーも取得されます。多数のプライベートデータメンバーがあり、STLコンテナーで使用されているオブジェクトを取得し、それをPIMPLクラスに変換する場合、元のオブジェクトと同じセマンティクスを提供するため、これがおそらく必要なものです。ただし、オブジェクトのコピーに動的メモリ割り当てが含まれるようになったため、オブジェクトをベクトルにプッシュするのはかなり遅くなることに注意してください。

このコピーの動作を望まない場合は、SampleImplのコピーが基になるImplオブジェクトを共有することもできます。この場合、どのSampleImplオブジェクトが基礎となるImplを所有しているかは、もはや明確ではありません(または明確に定義されていません)。所有権が明確に1つの場所に属していない場合、std :: auto_ptrはそれを保存するための間違った選択であり、他の何か、おそらくブーストテンプレートを使用する必要があります。

編集:上記のコピーコンストラクターと代入演算子は、例外をスローしない限り、~Impl例外に対して安全だと思います。とにかく、これは常にコードに当てはまるはずです。

于 2012-07-25T12:20:27.090 に答える