10

我在C++ FAQ上遇到了一个有趣的基类实现,根据我的天真理解,它可以作为一些智能指针实现(例如 shared_ptr)的替代方案。这是示例代码的逐字记录,但请按照上面的链接进行解释:

class Fred {
public:

  static Fred create1(std::string const& s, int i);
  static Fred create2(float x, float y);

  Fred(Fred const& f);
  Fred& operator= (Fred const& f);
 ~Fred();

  void sampleInspectorMethod() const;   // No changes to this object
  void sampleMutatorMethod();           // Change this object

  ...

private:

  class Data {
  public:
    Data() : count_(1) { }
    Data(Data const& d) : count_(1) { }              // Do NOT copy the 'count_' member!
    Data& operator= (Data const&) { return *this; }  // Do NOT copy the 'count_' member!
    virtual ~Data() { assert(count_ == 0); }         // A virtual destructor
    virtual Data* clone() const = 0;                 // A virtual constructor
    virtual void sampleInspectorMethod() const = 0;  // A pure virtual function
    virtual void sampleMutatorMethod() = 0;
  private:
    unsigned count_;   // count_ doesn't need to be protected
    friend class Fred; // Allow Fred to access count_
  };

  class Der1 : public Data {
  public:
    Der1(std::string const& s, int i);
    virtual void sampleInspectorMethod() const;
    virtual void sampleMutatorMethod();
    virtual Data* clone() const;
    ...
  };

  class Der2 : public Data {
  public:
    Der2(float x, float y);
    virtual void sampleInspectorMethod() const;
    virtual void sampleMutatorMethod();
    virtual Data* clone() const;
    ...
  };

  Fred(Data* data);
  // Creates a Fred smart-reference that owns *data
  // It is private to force users to use a createXXX() method
  // Requirement: data must not be NULL

  Data* data_;   // Invariant: data_ is never NULL
};

Fred::Fred(Data* data) : data_(data)  { assert(data != NULL); }

Fred Fred::create1(std::string const& s, int i) { return Fred(new Der1(s, i)); }
Fred Fred::create2(float x, float y)            { return Fred(new Der2(x, y)); }

Fred::Data* Fred::Der1::clone() const { return new Der1(*this); }
Fred::Data* Fred::Der2::clone() const { return new Der2(*this); }

Fred::Fred(Fred const& f)
  : data_(f.data_)
{
  ++data_->count_;
}

Fred& Fred::operator= (Fred const& f)
{
  // DO NOT CHANGE THE ORDER OF THESE STATEMENTS!
  // (This order properly handles self-assignment)
  // (This order also properly handles recursion, e.g., if a Fred::Data contains Freds)
  Data* const old = data_;
  data_ = f.data_;
  ++data_->count_;
  if (--old->count_ == 0) delete old;
  return *this;
}

Fred::~Fred()
{
  if (--data_->count_ == 0) delete data_;
}

void Fred::sampleInspectorMethod() const
{
  // This method promises ("const") not to change anything in *data_
  // Therefore we simply "pass the method through" to *data_:
  data_->sampleInspectorMethod();
}

void Fred::sampleMutatorMethod()
{
  // This method might need to change things in *data_
  // Thus it first checks if this is the only pointer to *data_
  if (data_->count_ > 1) {
    Data* d = data_->clone();   // The Virtual Constructor Idiom
    --data_->count_;
    data_ = d;
  }
  assert(data_->count_ == 1);

  // Now we "pass the method through" to *data_:
  data_->sampleMutatorMethod();
}

我没有看到任何 C++ 库中使用了这种方法。虽然它看起来很优雅。假设为单线程环境,为简单起见,请回答以下问题:

  1. 这是管理对象生命周期的智能指针方法的合适替代方案,还是只是自找麻烦?
  2. 如果合适,你为什么认为它不经常使用?
4

4 回答 4

8

这是管理对象生命周期的智能指针方法的合适替代方案,还是只是自找麻烦?

不,我不认为重新发明引用计数是一个好主意,特别是因为我们现在在 C++11 中有 std::shared_ptr。您可以根据 std::shared_ptr 轻松实现您可能具有多态引用计数的 Pimpl idiom 类。请注意,我们不再需要实现复制 ctor、赋值和 dtor,并且通过引用计数器和克隆,变异变得更简单:

// to be placed into a header file ...

#include <memory>
#include <utility>
#include <string>

class Fred
{
public:
    static Fred create1(std::string const& s, int i);
    static Fred create2(float x, float y);

    void sampleInspectorMethod() const;   // No changes to this object
    void sampleMutatorMethod();           // Change this object

private:
    class Data;
    std::shared_ptr<Data> data_;

    explicit Fred(std::shared_ptr<Data> d) : data_(std::move(d)) {}
};

...以及实施...

// to be placed in the corresponding CPP file ...

#include <cassert>
#include "Fred.hpp"

using std::shared_ptr;

class Fred::Data
{
public:
    virtual ~Data() {}                               // A virtual destructor
    virtual shared_ptr<Data> clone() const = 0;      // A virtual constructor
    virtual void sampleInspectorMethod() const = 0;  // A pure virtual function
    virtual void sampleMutatorMethod() = 0;
};

namespace {

class Der1 : public Fred::Data
{
public:
    Der1(std::string const& s, int i);
    virtual void sampleInspectorMethod() const;
    virtual void sampleMutatorMethod();
    virtual shared_ptr<Data> clone() const;
    ...
};

// insert Der1 function definitions here

class Der2 : public Data
{
public:
    Der2(float x, float y);
    virtual void sampleInspectorMethod() const;
    virtual void sampleMutatorMethod();
    virtual shared_ptr<Data> clone() const;
    ...
};

// insert Der2 function definitions here

} // unnamed namespace

Fred Fred::create1(std::string const& s, int i)
{
    return Fred(std::make_shared<Der1>(s,i));
}

Fred Fred::create2(float x, float y)
{
    return Fred(std::make_shared<Der2>(x,y));
}

void Fred::sampleInspectorMethod() const
{
    // This method promises ("const") not to change anything in *data_
    // Therefore we simply "pass the method through" to *data_:
    data_->sampleInspectorMethod();
}

void Fred::sampleMutatorMethod()
{
    // This method might need to change things in *data_
    // Thus it first checks if this is the only pointer to *data_
    if (!data_.unique()) data_ = data_->clone();
    assert(data_.unique());

    // Now we "pass the method through" to *data_:
    data_->sampleMutatorMethod();
}

(未经测试)

如果合适,你为什么认为它不经常使用?

我认为引用计数,如果你自己实现它,更容易出错。它还以在多线程环境中运行缓慢而著称,因为引用计数器必须以原子方式递增和递减。但我猜由于 C++11 提供了 shared_ptr 和 move 语义,这种写时复制模式可能会再次流行起来。如果您为 Fred 类启用移动语义,您可以避免一些以原子方式递增引用计数器的成本。因此,将 Fred 对象从一个位置移动到另一个位置应该比复制它更快。

于 2012-08-06T07:44:57.707 回答
4

这是管理对象生命周期的智能指针方法的合适替代方案,还是只是自找麻烦?

它是一种替代方案,但除非您有充分的理由使用它,否则它只是在重新发明轮子(以不可重用的方式)。

如果您将代码更改为使用 shared_ptr,您将无需显式定义复制/所有权语义(并在您的 pimpl 库中定义复制构造函数和赋值)。您还将使用已经定义和测试的代码(因为它是库的一部分)。

如果合适,你为什么认为它不经常使用?

因为 shared_ptr 可用并且已经实现了功能和所有“陷阱”。

于 2012-08-07T13:05:44.370 回答
3

我也是,我想知道它是否适合作为智能指针的替代品。

但是,IMO,要成为智能指针,类必须可用作指针,即:

SmartPtr<int> ptr = new int(42);
int x = *ptr;

所以是的,它是一种内存管理,但它不是智能指针,因为它没有指针的语义。

正如评论中提到的,pimpl idiom 对维护兼容性非常有帮助,而且它还可以促进开发,因为您不必重新编译包含的类。但是要获得后一个优势,您不能在父类内部定义内部类(即数据),而只需放置一个前向声明并将实际定义放在另一个标题中。

class Fred {
    ...
private:

class Data;

};

而且,我发现在 Fred 类中声明 Data 的变体对未来的开发没有用处,因为如果您需要添加另一个类,则需要修改 Fred 而不是仅仅创建另一个类。这可能是需要的,但我建议你避免这部分。

如果我有什么不清楚的地方,请随时提出问题!

于 2012-08-03T02:49:10.010 回答
3
  1. C++ FAQ 的答案似乎更像是一个如何管理共享数据的简单示例(使用写时复制)。缺少几个方面,这可能很重要。

  2. 不适用,我对 1 的看法。

为了避免“外部”引用计数引入的开销,您可以使用Andrei Alexandrescu 的Modern C++ Designstd::shared_ptr中描述的侵入式引用计数机制。Loki::COMRefCounted类展示了如何为 Windows 共享 COM 对象实现此类所有权策略。

本质上,它归结为智能指针模板类接受一个接口,该接口管理引用计数和检查delete指针类实例本身的可用性。我不知道 STD C++ 库是否支持为std::shared_ptr类实现这种策略覆盖。

我们非常成功地将Loki 库仅用于许多嵌入式项目中的智能指针模型。尤其是因为这个特性可以在效率的细粒度方面进行建模。

请注意,建议的(内置)实现默认情况下不是线程安全的。

如果上述所有方面都与您的目的无关,我建议您简单地std::shared_ptr表示您的Fred::Data班级,如 sellibitze 的回答所示。我也同意他在最后一段中提出的观点,引用计数和智能指针语义很容易被误解和错误地实现。

如果 C++11 标准或 boost 不适合您,loki 库仍然提供易于集成且强大的智能指针实现。

于 2012-08-06T18:44:40.837 回答