2

I need to create an Event object to be dispatched by an event listener system. The Event needs to have following properties:

  1. Event can potentially be handled by 0..n listener objects.
  2. Event contains a void pointer which can point to an arbitrary object (payload) (unknown type at build time). Event listener will convert to appropriate type depending on Event's name.
  3. Need the payload object to be (automatically) deleted once the event has been dispatched to interested parties. Original event raiser cannot deallocate as event goes into an asvnc queue.
  4. Assume listeners can make shallow copy of payload when event is processed.

I have implemented the solution here, but AFAIK this causes the payload to be deallocated (via the unique_ptr) after the first event handler.

In the code below, 'setData' attempts to take the payload object (dataObject), and to convert it into a shared_ptr to be carried by void* data. getData does the "reverse":

class Event {

public:
std::string name;

Event(std::string n = "Unknown", void* d = nullptr) :name(n), data(d) {}

template<class T>  void setData(const T dataObject)
{
    //Create a new object to store the data, pointed to by smart pointer
    std::shared_ptr<T> object_ptr(new T);
    //clone the data into the new object
    *object_ptr = dataObject;

    //data will point to the shared_pointer
    data= new std::shared_ptr<T>(object_ptr);
}


//reverse of setData.
template<class T>  T getData() const
{
    std::unique_ptr<
        std::shared_ptr<T>
        > data_ptr((std::shared_ptr<T>*) data);
    std::shared_ptr<T> object_ptr = *data_ptr;

    return *object_ptr;
}

private:
    void* data;
}; 
Community
  • 1
  • 1
Ken
  • 4,367
  • 4
  • 28
  • 41

1 Answers1

3

You should consider std::any instead of void*. That would avoid complex memory allocation for data. If you can't use C++17, it's not that hard to make your own implementation from Kevlin Henney's paper (add parts missing from C++17 specification, e.g. move constructor).

Your code may become something like that:

class Event {

public:
std::string name;

Event() :name("Unknown") {}

template<class T>
Event(std::string n, T&& dataObject) :name(n)
{
    setData(std::forward<T>(dataObject));
}

template<class T>  void setData(T&& dataObject)
{
    using data_t = typename std::decay<T>::type;
    data = std::make_shared<data_t>(std::forward<T>(dataObject));
}

//reverse of setData.
template<class T>  T getData() const
{
    using data_t = typename std::decay<T>::type;
    return *any_cast<std::shared<data_t>>(data);
}

private:
    any data;
};

I used in my code lvalue references in the template methods to avoid overloading: template deduction allows the same method to accept named variables as well as temporary values, with or without constness. See here for details.

std::forward is used to perform perfect forwarding. Indeed, if you construct an Event from an lvalue like this:

Event e{"Hello", Foo{}};

Calling setData without perfect forwarding will pass dataObject as an lvalue since it is a named variable in this context:

setData(dataObject); // will call Foo's copy constructor

Perfect forwarding will pass dataObject as an rvalue, but only if it was constructed from an rvalue in the first place:

setData(std::forward<T>(dataObject)); // will call Foo's move constructor

If dataObject had been constructed from an lvalue, the same std::forward will pass it as an lvalue, and yield copy constructor invokation, as wanted:

Foo f{};
Event e{"Hello", f};

// ...

setData(std::forward<T>(dataObject)); // will call Foo's copy constructor

Complete demo


If you want to keep using pointers to void, you can embed an appriopriate deleter with the shared_ptr:

template<class T>  void setData(T&& dataObject)
{
    using data_t = typename std::decay<T>::type;
    data = std::shared_ptr<void>(
        new data_t{std::forward<T>(dataObject)},
        [](void* ptr)
        {
            delete static_cast<data_t*>(ptr);
        }
    );
}

With data being declared as shared_ptr<void>, and getData:

template<class T>  T getData() const
{
    using data_t = typename std::decay<T>::type;
    return *std::static_pointer_cast<data_t>(data);
}
Community
  • 1
  • 1
rocambille
  • 15,398
  • 12
  • 50
  • 68
  • 1
    Wow!, there are number of type and pointer language features here that I was not aware of, will need a day or two to digest.. – Ken Nov 28 '16 at 21:10
  • 1
    Feel free to ask for explanations, so I could update my answer with more details. That would help future readers ;) – rocambille Nov 29 '16 at 07:19
  • First off: why the `forward()` call in ctor, what would happen if we just called `setData(dataObject);` in the ctor? – Ken Nov 29 '16 at 10:45
  • let me get this straight; the T&& and forward construct are all about efficiently dealing with rvalues, but not needed for the core functionality. The decay<> is to allow for arrays/fn_pointers, but again not needed in a minimal proof of concept? Just asking so I can boil this down to essentials. – Ken Dec 01 '16 at 10:23
  • this idea of embedding a deleter is also new to me , is there a name for this construct I could search. – Ken Dec 01 '16 at 12:33
  • 1
    There is no real name for this construct, that's a feature of `std::shared_ptr`. See [constructor's documentation](http://en.cppreference.com/w/cpp/memory/shared_ptr) (4-7). Note that the deleter is a fully accessible attribute: see [`get_deleter`](http://en.cppreference.com/w/cpp/memory/shared_ptr/get_deleter) – rocambille Dec 03 '16 at 09:45
  • The T&& with templates is to avoid overloading and so code duplication. `std::forward` is for efficiency. `std::decay` is more about removing const and reference qualifiers on type T – rocambille Dec 03 '16 at 09:50