Reference Counting in C++

2019-03-29 13:17发布

问题:

I'm implementing a math library in C++. The library will be compiled to a DLL so those who use it will only need the header files the classes' definitions.

The users of my classes will be people who are new to the language. However, there are some objects that might be referenced in several parts of their programs. Since I don't expect them to do the memory management, I'd like to do it myself. Therefore, I have to implement reference counting (garbage collection is not a possibility).

I want to make that reference counting as transparent as possible, for example...

// Define a Bézier curve
CVecList pts;
pts.Add(Vector(0,0,0));
pts.Add(Vector(0,0,100));
pts.Add(Vector(0,100,0));
pts.Add(Vector(0,100,100));
CCurve* c1 = new CBezier(pts);

// Define a 3rd order B-Spline curve
pts.Clear();
pts.Add(Vector(0,0,0));
pts.Add(Vector(0,200,100));
pts.Add(Vector(0,200,200));
pts.Add(Vector(0,-200,100));
pts.Add(Vector(0,-200,200));
pts.Add(Vector(0,0,0));
CCurve* c2 = new CBSpline(pts,3);

// The Bézier curve object must be deleted automatically
// because the only reference to it has been released
// Similar to IUnknown::Release() in COM
c1 = c2;

Things get a little bit more tricky when I define surface objects, because some surfaces are defined in terms of two curves:

CVecList pts;
// ...
CCurve* f = new CBezier(pts);

pts.Clear();
// ...
CCurve* g = new CBezier(pts);

// Mixed surface: S(u,v) = (1-v)*f(u) + v*g(u)
CSurface* s = new CMixed(f,g);

// There are two references to the first Bézier curve,
// the first one is f
// the second one is hidden in a member of CMixed

// Something similar applies to the second Bézier curve

I thought that overriding operator = for pointers could have helped:

// This is what I tried, but it's illegal:
typedef CReferenceCounted* PRC;
PRC& operator =(PRC& dest, PRC& source)
{
    if (source)
        source->AddRef();
    if (dest)
        dest->Release();
    memcpy(&dest,&source,sizeof(PRC));
    return dest;
}

... but then I found that operator = is not valid unless it is as a non-static member of a class.

Could anybody possible help me?

回答1:

What you tried was to overload an operator for scalar types. C++ doesn't allow you to do that except for enumerations (beside the point that operator= has to be a member). At least one of the types has to be a user defined type. Thus, what you want to do is to wrap the raw pointer into a user defined class, which overloads constructor, copy constructor, copy assignment operator and destructor an do the proper reference counting. This is an ideal situation for boost::shared_ptr, which does exactly that :

boost::shared_ptr<CCurve> c1(new CBezier(pts));

The same deal with surfaces:

CVecList pts;
// ...
boost::shared_ptr<CCurve> f(new CBezier(pts));

pts.Clear();
// ...
boost::shared_ptr<CCurve> g(new CBezier(pts));

// Mixed surface: S(u,v) = (1-v)f(u) + vg(u)
boost::shared_ptr<CSurface> s(new CMixed(f,g)); 

Carry around that smart pointer, and it will automatically manage the life-time of the pointed to object: If the last copy of the pointer goes out of scope, the object pointed to is freed. shared_ptr is designed to be easy to use. Try to avoid working with raw pointers as much as you can. Have a look at those smart pointers, they will ease your programmers live with C++ :)

Edit: If you are going to wrap a shared_ptr, you can do so using the pimpl (handle/body) idiom:

/* ---- wrapper in header file bezier.hpp */

struct CBezier {
    CBezier(CVecList const& list);
    void do_calc();
    // ...

private:
    struct CBezierImpl;
    boost::shared_ptr<CBezierImpl> p;
};

/* ---- implementation file bezier.cpp */

// private implementation
struct CBezier::CBezierImpl {
    CBezierImpl(CVecList const& list);
    void do_calc();
    // ...
};


CBezier::CBezier(CVecList const& list)
:p(new CBezierImpl(list)) {

}

void CBezier::do_calc() {
    // delegate to pimpl
    p->do_calc();
}

// ...


回答2:

If you're designing a math library, spend a lot of time thinking whether your classes can look like int or std::complex. That is to say, have values behave like values. E.g.

std::vector<math::point3d> pts;
pts.push_back(math::point3d(0,0,0));
pts.push_back(math::point3d(110,0,0));
pts.push_back(math::point3d(0,100,0));
pts.push_back(math::point3d(0,0,100));
CCurve c1 = make_bezier(pts);


回答3:

I'd recommend intrusive_ptr instead of shared_ptr for objects you can control for better performance and usability, as you can assign a raw pointer to intrusive_ptr later, because the reference count is embedded in the object.



回答4:

The users of my classes will be people who are new to the language.

Is your class designed for a programing course ?

If this is the case, I would avoid using pointers and use only copy constructors / assignation:

  • Performance / Memory is not a priority
  • Doing the memory management yourself will show a pretty bad example on how to use new/delete
  • Using any kind of smart pointer without knowing about memory management could cause a lot of confusion later on.


回答5:

I agree with Guishu and MSalters. Even if it's not for a programming course, it may be nice to imitate more closely maths look (e.g. vector3 = vector1+vector2 etc).

What you could also do is to use copy-on-write (refounting being a logical consequence), but only internally. That may give you fast-enough assignments, eliminate heap management on the client-side and similarity to math notation.

Note, however, that there are math libraries available for C++ (TNT, off the top of my head). Did you consider basing your work on that?