可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I came accross an intriguing implementation of a base class on the C++ FAQ that, according to my naive understanding, could serve as an alternative to some of the smart pointer implementations (e.g. shared_ptr). Here's the example code verbatim, but please follow the link above for an explanation:
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();
}
I don't see this approach being used in any C++ libraries; although it seems quite elegant. Assuming a single-threaded environment, for the sake of simplicity, please answer the following questions:
- Is this a suitable alternative to the smart pointer approach for managing the lifetime of objects, or is it just asking for trouble?
- If it is suitable, why do you suppose it's not used more often?
回答1:
Is this a suitable alternative to the smart pointer approach for managing the lifetime of objects, or is it just asking for trouble?
No, I don't think it's a good idea to reinvent reference counting especially since we have std::shared_ptr now in C++11. You can easily implement your possibly polymorphic reference-counted Pimpl idiom class in terms of std::shared_ptr. Notice how we don't have to implement copy ctor, assignment, dtor anymore and mutation gets simpler w.r.t. the reference counter and cloning:
// 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)) {}
};
...and the implementation...
// 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();
}
(untested)
If it is suitable, why do you suppose it's not used more often?
I think reference counting, if you implement it yourself, is easier to get wrong. It also has the reputation of being slow in multithreaded environments because the reference counters have to be incremented and decremented atomically. But I guess due to C++11 which offers shared_ptr and move semantics, this copy-on-write pattern might get a bit more popular again. If you enable move semantics for the Fred class you can avoid some of the costs of atomically incrementing reference counters. So moving a Fred object from one location to another should be even faster than copying it.
回答2:
Is this a suitable alternative to the smart pointer approach for managing the lifetime of objects, or is it just asking for trouble?
It is an alternative, but unless you have a very good reason for using it, it's just reinventing the wheel (in a non-reusable way).
If you change your code to use shared_ptr instead, you will have removed the need to define copy/ownership semantics explicitly (and to define copy constructor and assignment in your pimpl base). You will also use code that's already defined and tested (since it's part of the library).
If it is suitable, why do you suppose it's not used more often?
Because shared_ptr is available and already implements the functionality and all the "gotcha"s.
回答3:
Me too, I wonder if it's suitable as an alternative for smart pointer.
But, IMO, to be a smart pointer, a class must be usable as a pointer i.e. :
SmartPtr<int> ptr = new int(42);
int x = *ptr;
So yes, it it a sort of memory management, but it's not a smart pointer because it doesn't have the semantic of a pointer.
As mentioned in the comments, the pimpl idiom is really helpful for maintaining compatibility and it can also boost the development as you don't have to recompile the containing class.
BUT to have the latter advantage, you must not define the inner class (i.e. Data) inside of the parent class, but rather just put a forward declaration and put the actual definition inside another header.
class Fred {
...
private:
class Data;
};
And, I find it not useful for future development to declare variant of Data inside the class Fred, because if you need to add another class you will need to modify Fred instead of just creating another class. This could be wanted, but I suggest you to avoid that part.
If I wasn't clear about something, feel free to ask questions!
回答4:
The C++ FAQ's answer seems more to be a simplistic example how to manage shared data (using copy on write). There are several aspects missing, that might be important.
N/A with my opinion for 1.
To avoid the overhead introduced with 'external' reference counting as with std::shared_ptr
you can use an intrusive ref counting mechanism as described in Andrei Alexandrescu's book Modern C++ Design. The Loki::COMRefCounted class shows how to implement such ownership policy for Windows shared COM objects.
Essentially it boils down to the smart pointer template class accepting an interface that manages the reference counting and the check for delete
availability in the pointee class instance itself. I don't know if the STD C++ library supports to implement such policy override for the std::shared_ptr
class.
We're using the Loki Library solely for the smart pointer model in a number of embedded projects very sucessfully. Especially because of this feature to model fine granularity aspects of efficiency.
Note that the proposed (builtin) implementations aren't thread safe by default.
If all of that above aspects don't concern your purpose, I would propose to go for simple std::shared_ptr
representation of your Fred::Data
class as shown in sellibitze's answer. I also agree with the points he makes up in the last paragraph, reference counting and smart pointer semantics is prone to get it misunderstood and implemented wrong.
If the C++11 standard or boost aren't options to go for you, the loki library still provides an easy to integrate and robust smart pointer implementation.