头图
Last time, talked about the Observer mode in C++17, introduced the basic structure of this mode. Later, talked about the Observer mode in C++17 in an improved version is provided in 1614ff04b1b21e, which mainly focuses on scenes of violent use in a multi-threaded environment.
You can also visit original blog

Observer Pattern - Part III

Then we mentioned that for the observer mode, the native definition of GoF is of course an observer class, but for C++11 almost 15 years later, the observer's way of using a class definition is a bit out of date. . Especially after C++14/17, almost 23 years later, the support of lambda and std::function has become more stable, and closures or function objects can be easily wrapped without too many "advanced" techniques. The addition ability of the upper fold expression to the variable parameter template. So now there is a voice that believes that binding anonymous function objects or function objects directly to the observer is the correct way to open the observer mode.

So is it true?

The first thing we have to do is to realize this idea, and then use a test case to show the possibility of coding in this modal. Then let's take a look at its advantages and disadvantages.

Basic realization

This time the core class template we named it observable_bindable , because this is advantageous when you modify your own implementation code-you only need to add a suffix. This template class still uses a single structure S as the event/signal entity:

namespace hicc::util {

  /**
   * @brief an observable object, which allows a lambda or a function to be bound as the observer.
   * @tparam S subject or event will be emitted to all bound observers.
   * 
   */
  template<typename S>
  class observable_bindable {
    public:
    virtual ~observable_bindable() { clear(); }
    using subject_t = S;
    using FN = std::function<void(subject_t const &)>;

    template<typename _Callable, typename... _Args>
    observable_bindable &add_callback(_Callable &&f, _Args &&...args) {
      FN fn = std::bind(std::forward<_Callable>(f), std::forward<_Args>(args)...);
      _callbacks.push_back(fn);
      return (*this);
    }
    template<typename _Callable, typename... _Args>
    observable_bindable &on(_Callable &&f, _Args &&...args) {
      return add_callback(f, args...);
    }

    /**
     * @brief fire an event along the observers chain.
     * @param event_or_subject 
     */
    void emit(subject_t const &event_or_subject) {
      for (auto &fn : _callbacks)
        fn(event_or_subject);
    }

    private:
    void clear() {}

    private:
    std::vector<FN> _callbacks{};
  };

}

First of all, we do not provide member functions to unbind the observer. We think this is unnecessary, because the goal of this design was originally aimed at the lambda function, and it doesn't make much sense to unbind the lambda function. Of course, you can implement a remove_observer, which is not technically difficult, and you don’t even need to know C++ much, you can also get a copy.

Then with the help of the deduction ability of the std::bind function (the entire binding and type deduction, etc. at least c++14, c++17 is recommended), we provide a powerful and effective add_callback implementation. In addition, on() is Synonym for it.

The so-called powerful and effective means that we have realized the universal binding of various function objects on this single function signature. Whether anonymous functions, member functions, or ordinary functions, we can use add_callback( ) Is bound to the observable_bindable object.

New test code

How powerful it really is depends on the test code:

namespace hicc::dp::observer::cb {

  struct event {
    std::string to_string() const { return "event"; }
  };

  struct mouse_move_event : public event {};

  class Store : public hicc::util::observable_bindable<event> {};

} // namespace hicc::dp::observer::cb

void fntest(hicc::dp::observer::cb::event const &e) {
  hicc_print("event CB regular function: %s", e.to_string().c_str());
}

void test_observer_cb() {
  using namespace hicc::dp::observer::cb;
  using namespace std::placeholders;

  Store store;

  store.add_callback([](event const &e) {
    hicc_print("event CB lamdba: %s", e.to_string().c_str());
  }, _1);
  
  struct eh1 {
    void cb(event const &e) {
      hicc_print("event CB member fn: %s", e.to_string().c_str());
    }
    void operator()(event const &e) {
      hicc_print("event CB member operator() fn: %s", e.to_string().c_str());
    }
  };
  
  store.on(&eh1::cb, eh1{}, _1);
  store.on(&eh1::operator(), eh1{}, _1);

  store.on(fntest, _1);

  store.emit(mouse_move_event{});
}

Note that the binding syntax of this on()/add_callback() is similar to std::bind , you may need placeholders such as std::placeholder::_1, _2, .. You also need to note that this binding syntax completely follows the various special capabilities of std::bind, such as early binding technology. However, some of these capabilities cannot be directly reflected in on(), and some are useful but not used. In addition, the observer pattern is still being discussed here, and the existing display is sufficient.

The output is similar to this:

--- BEGIN OF test_observer_cb                         ----------------------
09/19/21 08:38:02 [debug]: event CB lamdba: event ...
09/19/21 08:38:02 [debug]: event CB member fn: event ...
09/19/21 08:38:02 [debug]: event CB member operator() fn: event 
09/19/21 08:38:02 [debug]: event CB regular function: event 
--- END OF test_observer_cb                           ----------------------

It took 465.238us

So there is no surprise, even whether it is observable_bindable or the use case code is unexpectedly concise and straightforward, and intuitionistic.

This example can remind us that GoF is of course the classic in the classics (no way, it came out early, when it came out, our minds did not turn to this kind of summary direction at all, but it was a classic, I was doing it at that time What mile, oh, I did some scada debugging in a substation in a deep mountain in Guizhou where birds don’t eggs, let’s get into the details), but 1614ff04b1b50d may not necessarily be the ancestor’s law .

Improve

The above seems quite perfect, but there is a small problem. The binding syntax of Kang Kang test code, for example:

store.on(fntest, _1);

That ugly _1 is very dazzling. Can it be destroyed?

Because the interface of our agreed callback function is:

using FN = std::function<void(subject_t const &)>;

So this _1 corresponds to subject_t const & , which is the calling convention of std::bind. Note that the signature of the callback function is fixed, so we do have a way to eliminate _1 , that is, modify the code of add_callback()

template<typename _Callable, typename... _Args>
observable_bindable &add_callback(_Callable &&f, _Args &&...args) {
  FN fn = std::bind(std::forward<_Callable>(f), std::forward<_Args>(args)..., std::placeholders::_1);
  _callbacks.push_back(fn);
  return (*this);
}

If we add it, there is no need to write it in the user code, right?

This method is a bit rascal, but it works. Then the test code is like this:

    store.add_callback([](event const &e) {
        hicc_print("event CB lamdba: %s", e.to_string().c_str());
    });

    store.on(&eh1::cb, eh1{});
    store.on(&eh1::operator(), eh1{});

    store.on(fntest);

Isn’t it much better?

This example tells us that writing C++ sometimes does not need the so-called exquisite, exquisite, wonderful, and whimsical tricks.

The fighters are so ineffective, and they are just plain unremarkable.

Pros and cons

In short, we have implemented this main functional style observer mode. You still need to observable_bindable , but you can establish observations on it anytime, anywhere, you can use anonymous lambdas, or use various styles of named function objects.

As for the shortcomings, it is not too much. Perhaps it is not so easy to remove_callback when using lambda. This may be a bit inappropriate for some scenarios that require reentrancy, but this can be easily solved by explicitly named.

There are two ways to explicitly name, one is to pay the lambda to a variable:

auto my_observer = [](event const &e) {
  hicc_print("event CB lamdba: %s", e.to_string().c_str());
};
store.on(my_observer);
store.emit(event{});
store.remove(my_observer);
remove() needs to be implemented by you

The other method is ordinary function objects, or class member function objects, or class static member function objects, which are all naturally named. The example is skipped.

postscript

The whole process is very simple, even simpler than I expected, of course I still ran into some trouble.

The main trouble is that the formal parameter list in the function signature needs to be correctly transmitted to the calling part of emit. But you have also seen that the final code actually completely relies on the ability of automatic derivation, which unexpectedly solves the problem directly. Of course I ran into trouble, took some detours, and tried to ask for help from the variable parameter template ability and the power of traits. It turns out that I don't have to be so tiring, and there is no need for such a complicated parameter list. And auto can often hit the world.

Callable technology, I also have the same application in pool, ticker, etc., but there is a little difference in those times. At that time, the function object to be bound does not have a parameter list. When doing function binding in cmdr-cxx, what I am facing is a parameter table with a certain format.

But none of these matter.

:end:


hedzr
95 声望19 粉丝