Подсчет ссылок в C++ OO-Style

В FAQ по C++ я натолкнулся на интригующую реализацию базового класса, которая, согласно моему наивному пониманию, могла бы служить альтернативой некоторым реализациям интеллектуальных указателей (например, 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 ответа

Решение

Является ли это подходящей альтернативой подходу интеллектуального указателя для управления временем жизни объектов, или это просто вызывает проблемы?

Нет, я не думаю, что это хорошая идея, чтобы заново изобрести подсчет ссылок, тем более что у нас теперь есть std::shared_ptr в C++11. Вы можете легко реализовать свой, возможно, полиморфный класс Pimpl с подсчетом ссылок в терминах std::shared_ptr. Обратите внимание, что нам больше не нужно реализовывать копирование ctor, assignment, 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 и семантику перемещения, этот шаблон копирования при записи может снова стать немного более популярным. Если вы включите семантику перемещения для класса Fred, вы сможете избежать некоторых затрат на атомарное увеличение счетчиков ссылок. Поэтому перемещение объекта Fred из одного места в другое должно быть даже быстрее, чем его копирование.

  1. Ответ C++ FAQ кажется более упрощенным примером того, как управлять общими данными (используя копирование при записи). Не хватает нескольких аспектов, которые могут быть важны.

  2. N / A с моим мнением для 1.

Чтобы избежать накладных расходов, связанных с "внешним" подсчетом ссылок, как при std::shared_ptr Вы можете использовать навязчивый механизм подсчета ссылок, как описано в книге Андрея Александреску Modern C++ Design. Класс Loki::COMRefCounting показывает, как реализовать такую ​​политику владения для общих COM-объектов Windows.

По сути, это сводится к тому, что класс шаблона интеллектуального указателя принимает интерфейс, который управляет подсчетом ссылок и проверкой delete наличие в самом экземпляре класса pointee. Я не знаю, поддерживает ли библиотека STD C++ для реализации такого переопределения политики для std::shared_ptr учебный класс.

Мы используем библиотеку Loki исключительно для модели интеллектуальных указателей во многих встроенных проектах. Особенно из-за этой функции для моделирования тонкой гранулярности аспекты эффективности.

Обратите внимание, что предлагаемые (встроенные) реализации не являются потокобезопасными по умолчанию.

Если все вышеперечисленные аспекты не имеют отношения к вашей цели, я бы предложил перейти к простой std::shared_ptr представление вашего Fred::Data класс, как показано в ответе Sellibitze. Я также согласен с замечаниями, которые он сформулировал в последнем абзаце, - подсчет ссылок и семантика умных указателей склонны к неправильному пониманию и реализации неправильно.

Если стандарт C++11 или boost не подходят для вас, библиотека loki по-прежнему предоставляет простую в интеграции и надежную реализацию интеллектуальных указателей.

Является ли это подходящей альтернативой подходу интеллектуального указателя для управления временем жизни объектов, или это просто вызывает проблемы?

Это альтернатива, но если у вас нет веских причин для ее использования, она просто изобретает велосипед (без возможности повторного использования).

Если вы измените свой код на использование shared_ptr, вы избавитесь от необходимости явно определять семантику копирования / владения (и определять конструктор и назначение копирования в вашей базе данных pimpl). Вы также будете использовать код, который уже определен и протестирован (так как он является частью библиотеки).

Если это подходит, почему вы думаете, что он не используется чаще?

Потому что shared_ptr доступен и уже реализует функциональность и все "гоча".

Мне тоже интересно, подходит ли она как альтернатива для умного указателя.

Но, IMO, чтобы быть умным указателем, класс должен использоваться в качестве указателя, т.е.

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

Так что да, это своего рода управление памятью, но это не умный указатель, потому что он не имеет семантики указателя.

Как упоминалось в комментариях, идиома pimpl действительно полезна для поддержания совместимости, и она также может ускорить разработку, поскольку вам не нужно перекомпилировать содержащий класс. НО, чтобы иметь последнее преимущество, вы не должны определять внутренний класс (т. Е. Data) внутри родительского класса, а просто поместить предварительное объявление и поместить фактическое определение в другой заголовок.

class Fred {
    ...
private:

class Data;

};

И я считаю, что для дальнейшей разработки бесполезно объявлять вариант Data внутри класса Fred, потому что если вам нужно добавить другой класс, вам нужно будет модифицировать Fred вместо простого создания другого класса. Это может быть желательно, но я предлагаю вам избежать этой части.

Если я чего-то не понял, не стесняйтесь задавать вопросы!

Другие вопросы по тегам