开发者

C++ Covariance and references

Lets say I have an abstract base class with a pure virtual that returns an 开发者_JAVA技巧expensive object. As it's an expensive object, I should return a reference to it.

But life's not that simple, let's say I have two classes derived from it: one has the function called often, so it is more efficient to store a copy in the instance and return a reference. The other is called rarely, so it is better to create the object on demand to save RAM.

I thought I could just use covariance because the Liskov substitution principle would be happy, but of course Obj is not a subtype of Obj&, so compile errors result.

class abc
{
public:
    virtual BigObj& obj() = 0;
};

class derived : public abc
{
public:
    ...
    virtual BigObj obj() { return obj_; }

private:
    BigObj obj_;
};

Results in:

conflicting return type specified for ‘virtual BigObj derived::obj()’

Is there a more elegant solution to this than simply picking the least worst?


One solution is to create a smart pointer class to manage BigObj*s:

class BigObjPtr {
public:
    BigObjPtr(bool del, BigObj* ptr) : del(del), ptr(ptr) { }

    BigObj* operator->() {
        return ptr;
    }

    virtual ~BigObjPtr() {
        if (del) delete ptr;
    }

private:
    BigObj* ptr;
    bool del;
};

Then change your classes to return one of these, and set the del bool to whether you want the BigObjPtr to destroy its pointer when it goes out of scope:

class abc
{
public:
    virtual BigObjPtr obj() = 0;
};

class derived : public abc
{
public:
    ...
    BigObjPtr obj() { return BigObjPtr(false, &obj_); }

private:
    BigObj obj_;
};

class otherderived : public abc
{
public:
    ...
    BigObjPtr obj() { return BigObjPtr(true, new BigObj); }
};

You'd of course need to manage copying of BigObjPtrs, etc. but I leave that to you.


You should rethink your assumptions, the interface of the functions should be defined in terms of what the semantics are. So the main question is what are the semantics of the function?

If your function creates an object, regardless of how big or small it is to copy you should return by value regardless of how often the code is called. If on the other hand, what you are doing is providing access to an already existing object, then you should return a reference to the object, in both cases.

Note that:

expensive function() {
   expensive result;
   return result;
}
expensive x = function();

Can be optimized by the compiler into a single expensive object (it can avoid copying from result to the returned object and elide the copy from the returned object into x).

On the Liskov substitution principle, you are not following here, one type is returning an object, and the other is returning a reference to an object, which are completely different things in many aspects, so even if you can apply similar operations to the two returned types, the fact is that there are other operations that are not the same, and there are different responsibilities that are passed to the caller.

For example, if you modify the returned object in the reference case, you are changing the value of all returned objects from the function in the future, while in the value case the object is owned by the caller and it does not matter what the caller does to its copy, the next call to the function will return a newly created object without those changes.

So again, think on what the semantics of your function are and use that to define what to return in all deriving classes. If you are not sure how expensive the piece of code is, you can come back with a simplified use case and we can discuss how to improve the performance of the application. For that you will need to be explicit in what user code does with the objects it gets, and what it expects from the function.


Return a shared_ptr<BigObj> instead. One class can keep it's own copy around, the other can create it on demand.


You have two options:

  1. As you noted, you can pick the least worst. That is, pick BigObj or BigObj& for both derived classes.

  2. You could add new methods to the derived classes that have the appropriate return types. eg, BigObj& obj_by_ref() and BigObj obj_by_val().

The reason you can't have it both ways is because you might have a pointer to abc directly. It specifies BigObj&, so no matter which class provides the implementation, it had better return an BigObj&, because that's what the call site expects. If a misbehaving subclass returned an BigObj directly, it would cause mayhem when the compiler tried to use it as a reference!


Return by reference is dangerous in most cases as it can lead to hard to track memory issues, for example when the parent object goes out of scope or deleted. I would redesign the BigObj to be a simple delegate (or container) class that actually holds the pointer to expensive object.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜