C++ safe idiom to call a member function of a class through a shared_ptr class member

耗尽温柔 提交于 2019-12-13 02:55:30

问题


Problem description

In designing an observer pattern for my code, I encountered the following task: I have a class Observer which contains a variable std::shared_ptr<Receiver> and I want to use a weak_ptr<Receiver> to this shared-pointer to safely call a function update() in Observer (for a more detailed motivation including some profiling measurements, see the EDIT below).

Here is an example code:

struct Receiver
{
    void call_update_in_observer() { /* how to implement this function? */}
};

struct Observer
{
    virtual void update() = 0;
    std::shared_ptr<Receiver> receiver;
};

As mentioned there is a weak_ptr<Receiver> from which I want to call Observer::update() -- at most once -- via Receiver::call_update_in_observer():

Observer observer;
std::weak_ptr<Receiver> w (observer.receiver);

auto s = w.lock();
if(s)
{
    s->call_update_in_observer(); //this shall call at most once Observer::update()
                                  //regardless how many copies of observer there are
}

(Fyi: the call of update() should happen at most once because it updates a shared_ptr in some derived class which is the actual observer. However, whether it is called once or more often does not affect the question about "safeness" imo.)

Question:

  • What is an appropriate implementation of Observer and Receiver to carry out that process in a safe manner?


Solution attempt

Here is an attempt for a minimal implementation -- the idea is that Receiver manages a set of currently valid Observer objects, of which one member is called:

struct Receiver
{
    std::set<Observer *> obs;
    void call_update_in_observer() const
    {
        for(auto& o : obs)
        {
            o->update();
            break;  //one call is sufficient
        }
    }
};

The class Observer has to take care that the std::shared_ptr<Receiver> object is up-to-date:

struct Observer
{
    Observer()
    {
        receiver->obs.insert(this);
    }

    Observer(Observer const& other) : receiver(other.receiver)
    {
        receiver->obs.insert(this);
    }

    Observer& operator=(Observer rhs)
    {
        std::swap(*this, rhs);
        return *this;
    }

    ~Observer()
    {
        receiver->obs.erase(this);
    }

    virtual void update() = 0;

    std::shared_ptr<Receiver> receiver = std::make_shared<Receiver>();
};

DEMO

Questions:

  • Is this already safe? -- "safe" meaning that no expired Foo object is called. Or are there some pitfalls which have to be considered?

  • If this code is safe, how would one implement the move constructor and assignment?

(I know this has the feeling of being appropriate for CodeReview, but it's rather about a reasonable pattern for this task than about my code, so I posted it here ... and further the move constructors are still missing.)



EDIT: Motivation

As the above requirements have been called "confusing" in the comments (which I can't deny), here is the motivation: Consider a custom Vector class which in order to save memory performs shallow copies:

struct Vector
{
    auto operator[](int i) const { return v[i]; }
    std::shared_ptr<std::vector<double> > v;
};

Next one has expression template classes e.g. for the sum of two vectors:

template<typename _VectorType1, typename _VectorType2>
struct VectorSum
{
    using VectorType1 = std::decay_t<_VectorType1>;
    using VectorType2 = std::decay_t<_VectorType2>;

    //alternative 1: store by value
    VectorType1 v1;
    VectorType2 v2;

    //alternative 2: store by shared_ptr
    std::shared_ptr<VectorType1> v1;
    std::shared_ptr<VectorType2> v2;

    auto operator[](int i) const
    {
        return v1[i] + v2[i];
    }
};

//next overload operator+ etc.

According to my measurements, alternative 1 where one stores the vector expressions by value (instead of by shared-pointer) is faster by a factor of two in Visual Studio 2015. In a simple test on Coliru, the speed improvement is even a factor of six:

type                      Average access time        ratio
--------------------------------------------------------------
Foo                     : 2.81e-05                   100%
std::shared_ptr<Foo>    : 0.000166                   591%
std::unique_ptr<Foo>    : 0.000167                   595%
std::shared_ptr<FooBase>: 0.000171                   611%
std::unique_ptr<FooBase>: 0.000171                   611%

The speedup appears particularly when operator[](int i) does not perform expensive calculations which would make the call overhead negligible.

Consider now the case where an arithmetic operation on a vector expression is too expensive to calculate each time anew (e.g. an exponential moving average). Then one needs to memoize the result, for which as before a std::shared_ptr<std::vector<double> > is used.

template<typename _VectorType>
struct Average
{
    using VectorType = std::decay_t<_VectorType>;

    VectorType v;
    std::shared_ptr<std::vector<double> > store;

    auto operator[](int i) const
    {
        //if store[i] is filled, return it
        //otherwise calculate average and store it.
    }
};

In this setup, when the vector expression v is modified somewhere in the program, one needs to propagate that change to the dependent Average class (of which many copies can exists), such that store is recalculated -- otherwise it will contain wrong values. In this update process, however, store needs to be recalculated only once, regardless how many copies of the Average object exist.

This mix of shared-pointer and value semantics is the reason why I'm running in the somewhat confusing situation as above. My solution attempt is to enforce the same cardinality in the observer as in the updated objects -- this is the reason for the shared_ptr<Receiver>.

来源:https://stackoverflow.com/questions/35160431/c-safe-idiom-to-call-a-member-function-of-a-class-through-a-shared-ptr-class-m

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!