The talks about the Observer mode in C++17, the introduced the basic structure of the mode. Later, talked about the Observer mode in C++17 which mainly focused on scenes of violent use in a multi-threaded environment. Later, there was an article on Observer mode in C++17-Supplement /3 , which talked about the solution of directly binding lambda as an observer.
Observer Pattern - Part IV
So, I think this fourth article, in any case, has to reproduce an independent realization of Qt's Slot signal slot model. And after this re-engraving is finished, the observer mode must come to an end. After all, I really took a lot of trouble with this pattern, and it's time to end.
Do you want to make Rx lightweight version? This is a problem.
Original reference
Speaking of Qt's signal slot mode, it can be regarded as famous. It is strong in that it can ignore the function signature and bind it as you want (not completely random, but it is also very possible). The UI event push and trigger from the sender to the receiver is clearer and cleaner, and you don’t have to be constrained by it. Deterministic function signature.
Deterministic function signatures. Microsoft's MFC and even ATL and WTL are used in the UI message pump part. They also use macros to solve the binding problem, which is really full of backwardness.
To say that in the past, MFC was also popular, Qt can only be quietly shrunk in a corner, even if Qt has a lot of good designs. So what? There are many excellent designs of our MFC, especially ATL/WTL. So this is another ancient history of technology and market recognition.
Okay, just spit it out.
The problem with Qt lies in two points: one is the licensing system that has been ambiguous, and the other is private extensions that people can’t love. From qmake to qml to various MOC extensions on C++, it really makes Pure C++ Pie is very upset.
Of course, Qt is not as niche as we thought. In fact, its audience is still very large. It occupies at least a strong part of cross-platform UI and the main part of UI development for embedded devices. .
First of all, the signal slot is a unique core mechanism of Qt. It is supported by the basic class QObject. It is actually to complete the communication between objects, not just the distribution of UI events. However, considering the core mechanism and logic of this communication mechanism, we believe that it is still a manifestation of the observer mode, or a mechanism for subscribers to read specific signals from the publisher.
The key to the signal slot algorithm is that it believes that no matter how a function is converted, it can always be turned into a function pointer and placed in a certain slot. Each QObject (Qt's basic class) can manage such a slot as needed. surface.
bool QObject::connect ( const QObject * sender, const char * signal, const QObject * receiver, const char * member ) [static]
When transmitting a signal, the object scans each slot, and then deforms the signal as needed (matches the actual parameter list of the bound function) and calls back the bound function, especially if the bound function is For member functions of a certain class instance, the correct this pointer will also be quoted to ensure that the callback is completed.
Qt uses a keyword signals to specify signals:
signals:
void mySignal();
void mySignal(int x);
void mySignalParam(int x,int y);
This is obviously weird (just get used to it). And Qt has a lot of weirdness, so this is also the fundamental reason why it can't become popular. If it is too closed, everyone is not willing to play with you.
So about the slot, the slot function is an ordinary C++ function, and its unusual feature is that there will be some signals associated with it. The associated methods are QObject::connect and disconnect, the prototypes have been given above.
An example snippet to show how the signal slot is used:
QLabel *label = new QLabel;
QScrollBar *scroll = new QScrollBar;
QObject::connect( scroll, SIGNAL(valueChanged(int)),
label, SLOT(setNum(int)) );
SIGNAL and SLOT are macros, they will use Qt's internal implementation to complete the conversion work.
Summary
We are not going to teach Qt development knowledge, let alone Qt's internal implementation mechanism, so it is enough to extract such an example.
If you are learning or want to understand Qt development knowledge, please refer to their official website, and can focus on understanding the meta object compiler MOC (meta object compiler), Qt relies on this thing to solve its proprietary non-c++ extensions, such as signals and many more.
Basic realization
Now let's recreate the C++17 implementation of a set of signal slots. Of course, we don't consider any other related concepts of Qt, but only implement the content related to the observer mode of subscribing, transmitting signals, and receiving signals.
The re-engraved version does not copy Qt's connect interface style as it is.
We need to rethink what should be the main focus and what kind of grammar should be used.
The first thing to be sure is that an observable object is also a slot manager and also a signal emitter. As a slot manager, each slot can contain M connected slot entries, that is, observers. Since an observable object manages a single slot, if you want many slots, you need to derive multiple observable objects.
In any case, after retrieving the essence of the signal slot, our signal-slot implementation is actually talking about the Observer mode in C++17 almost exactly the same-except that signal-slot needs to support variable Outside of the function parameter list.
signal<SignalSubject...>
Since it is a signal transmitter, our observable object is called signal with variable SignalSubject... template parameters. A signal<int, float>
allows two parameters of int and float to be used when transmitting the signal: sig.emit(1, 3.14f)
. Of course, you can change the int to a composite object, because it is a variable parameter, so you don't even need to take a specific parameter, at this time, the signal is transmitted as if it is just a trigger function.
This is our realization:
namespace hicc::util {
/**
* @brief A covered pure C++ implementation for QT signal-slot mechanism
* @tparam SignalSubjects
*/
template<typename... SignalSubjects>
class signal {
public:
virtual ~signal() { clear(); }
using FN = std::function<void(SignalSubjects &&...)>;
template<typename _Callable, typename... _Args>
signal &connect(_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>
signal &on(_Callable &&f, _Args &&...args) {
FN fn = std::bind(std::forward<_Callable>(f), std::forward<_Args>(args)...);
_callbacks.push_back(fn);
return (*this);
}
/**
* @brief fire an event along the observers chain.
* @param event_or_subject
*/
signal &emit(SignalSubjects &&...event_or_subjects) {
for (auto &fn : _callbacks)
fn(std::move(event_or_subjects)...);
return (*this);
}
signal &operator()(SignalSubjects &&...event_or_subjects) { return emit(event_or_subjects...); }
private:
void clear() {}
private:
std::vector<FN> _callbacks{};
};
} // namespace hicc::util
connect() imitates Qt's interface name, but we recommend its synonym on() to bind function entities.
The above implementation is not as complicated as the known open source implementation. In fact, many of the exquisite C++ open source metaprogramming codes are a bit crazy. Traits are used too much, and the splits are too powerful. My brain memory is small, and I can't get it.
Speaking of our implementation, there is basically nothing to say. Adhering to the previous implementation idea, discarding the explicit slot entity design scheme, simply packaging the user function as FN
as a slot function. This does not have some of the comprehensiveness of Qt, but in fact, modern society does not need any ingenuity to satisfy the Qt class system. It is purely over-designed.
Tests
Then look at the test program:
namespace hicc::dp::observer::slots::tests {
void f() { std::cout << "free function\n"; }
struct s {
void m(char *, int &) { std::cout << "member function\n"; }
static void sm(char *, int &) { std::cout << "static member function\n"; }
void ma() { std::cout << "member function\n"; }
static void sma() { std::cout << "static member function\n"; }
};
struct o {
void operator()() { std::cout << "function object\n"; }
};
inline void foo1(int, int, int) {}
void foo2(int, int &, char *) {}
struct example {
template<typename... Args, typename T = std::common_type_t<Args...>>
static std::vector<T> foo(Args &&...args) {
std::initializer_list<T> li{std::forward<Args>(args)...};
std::vector<T> res{li};
return res;
}
};
} // namespace hicc::dp::observer::slots::tests
void test_observer_slots() {
using namespace hicc::dp::observer::slots;
using namespace hicc::dp::observer::slots::tests;
using namespace std::placeholders;
{
std::vector<int> v1 = example::foo(1, 2, 3, 4);
for (const auto &elem : v1)
std::cout << elem << " ";
std::cout << "\n";
}
s d;
auto lambda = []() { std::cout << "lambda\n"; };
auto gen_lambda = [](auto &&...a) { std::cout << "generic lambda: "; (std::cout << ... << a) << '\n'; };
UNUSED(d);
hicc::util::signal<> sig;
sig.on(f);
sig.connect(&s::ma, d);
sig.on(&s::sma);
sig.on(o());
sig.on(lambda);
sig.on(gen_lambda);
sig(); // emit a signal
}
void test_observer_slots_args() {
using namespace hicc::dp::observer::slots;
using namespace std::placeholders;
struct foo {
void bar(double d, int i, bool b, std::string &&s) {
std::cout << "memfn: " << s << (b ? std::to_string(i) : std::to_string(d)) << '\n';
}
};
struct obj {
void operator()(float f, int i, bool b, std::string &&s, int tail = 0) {
std::cout << "obj.operator(): I was here: ";
std::cout << f << ' ' << i << ' ' << std::boolalpha << b << ' ' << s << ' ' << tail;
std::cout << '\n';
}
};
// a generic lambda that prints its arguments to stdout
auto printer = [](auto a, auto &&...args) {
std::cout << a << std::boolalpha;
(void) std::initializer_list<int>{
((void) (std::cout << " " << args), 1)...};
std::cout << '\n';
};
// declare a signal with float, int, bool and string& arguments
hicc::util::signal<float, int, bool, std::string> sig;
connect the slots
sig.connect(printer, _1, _2, _3, _4);
foo ff;
sig.on(&foo::bar, ff, _1, _2, _3, _4);
sig.on(obj(), _1, _2, _3, _4);
float f = 1.f;
short i = 2; // convertible to int
std::string s = "0";
// emit a signal
sig.emit(std::move(f), i, false, std::move(s));
sig.emit(std::move(f), i, true, std::move(s));
sig(std::move(f), i, true, std::move(s)); // emit diectly
}
Similarly, the familiar std::bind support capability will not be repeated here.
test_observer_slots is an example of a signal without parameters, and test_observer_slots_args demonstrates how the signal is emitted with four parameters. Unfortunately, you may sometimes have to bring std::move. I may not find this problem someday in the future. It can be resolved within a period of time, but you are welcome to vote for me through the ISSUE system of hicc-cxx.
optimization
This time, the function parameter list is variable. There is not only one _1
, nor can it predict how many parameters there will be, so the depends on the method and now it doesn’t work. Can only honestly seek whether there is a way to automatically bind placeholders. Unfortunately for std::bind, std::placeholders is an absolutely indispensable support, because std::bind allows you to specify the order of binding parameters when binding and to bind preset values in advance. Because of this design goal, you cannot erase _1
and so on.
In case when you find a way, it also means that you have given up all the benefits brought by placeholders such as _1
So this will be a difficult decision. By the way, BTW, English does not have the term "difficult decision" at all, it only says "that decision will be very difficult". In short, English actually cannot accurately describe the difficulty of a decision, such as: "a little bit difficult", "a little bit difficult", "a little bit difficult", "a little bit difficult", "just like in the past nine It's as hard as eighteen bends",....... It can be "a little" and "a little bit" at first, but it must be dead later, right... Am I digressing again.
About std::bind and std::placeholders, some people have been complaining about the indispensable stories. However, supporters always talk about the importance of A (partial) callable entity, regardless of the practicality of the other: it is possible to have an interface such as std::connect or std::link to allow automatic binding of Callbacks And automatically fill in the default value (ie zero value) of the formal parameter.
There are probably two ways that can work.
One is to decompose different function objects, bind and forward variable parameters separately, which will be a bit huge small project-because it will re-implement a std::bind and provide the additional ability of automatic binding.
The other is the method we will adopt. We generally keep the original ability of relying on std::bind, but also use the method of adding placeholder arguments last time.
cool::bind_tie
However, as mentioned in the previous article, I don't know how many SignalSubjects template parameters the user is going to instantiate, so simply adding placeholders will not work. So we slightly changed our thinking and added 9 placeholders at once, but added a layer of template function expansion. In the new layer of template function, we only take out just as many parameter packs as SubjectsCount from the callee and pass them to std. ::bind is satisfied.
A verifiable prototype is:
template<typename Function, typename Tuple, size_t... I>
auto bind_N(Function &&f, Tuple &&t, std::index_sequence<I...>) {
return std::bind(f, std::get<I>(t)...);
}
template<int N, typename Function, typename Tuple>
auto bind_N(Function &&f, Tuple &&t) {
// static constexpr auto size = std::tuple_size<Tuple>::value;
return bind_N(f, t, std::make_index_sequence<N>{});
}
auto printer = [](auto a, auto &&...args) {
std::cout << a << std::boolalpha;
(void) std::initializer_list<int>{
((void) (std::cout << " " << args), 1)...};
std::cout << '\n';
};
// for signal<float, int, bool, std::string> :
template<typename _Callable, typename... _Args>
auto bind_tie(_Callable &&f, _Args &&...args){
using namespace std::placeholders;
return bind_N<4>(printer, std::make_tuple(args...));
}
bind_tie(printer, _1,_2,_3,_4,_5,_6,_7,_8,_9);
Here we assume some premises to simulate the deployment location of signal<...>
- For the printer, it requires 4 parameters, but we give it 9 parameters.
- Then in
bind_tie()
, 9 placeholders are condensed into a tuple, this is for the next layer to be able to continue processing. - The N version of the next layer
bind_N()
is mainly used to generate a natural number sequence at compile time. This is achieved bystd::make_index_sequence<N>{}
, which generates a 1..N sequence - In the
bind_N()
without N in 0615125ad31dec, the parameter pack expansion capability is used. It usesstd::get<I>(t)...
expansion to extract 4 out of 9 placeholders in the tuple - Our goal has been achieved
In this process, there is a little loss of memory and time, because make_tuple
needed. But compared with the semantics of the grammar, this loss can afford.
In this way, we can rewrite signal::connect()
to bind_tie
version:
static constexpr std::size_t SubjectCount = sizeof...(SignalSubjects);
template<typename _Callable, typename... _Args>
signal &connect(_Callable &&f, _Args &&...args) {
using namespace std::placeholders;
FN fn = cool::bind_tie<SubjectCount>(std::forward<_Callable>(f), std::forward<_Args>(args)..., _1, _2, _3, _4, _5, _6, _7, _8, _9);
_callbacks.push_back(fn);
return (*this);
}
Note that we extracted the number from the signal template parameter SignalSubjects
sizeof...(SignalSubjects)
syntax.
Also supports the binding of member functions
There is one last problem. When faced with member functions, connect will fail:
sig.on(&foo::bar, ff);
The solution is to make a second set of bind_N specialization, allowing std::is_member_function_pointer_v
and special processing. In order for the two specialization versions to coexist correctly, the template parameter definition semantics of std::enable_if
The final cool::bind_tie
as follows:
namespace hicc::util::cool {
template<typename _Callable, typename... _Args>
auto bind(_Callable &&f, _Args &&...args) {
return std::bind(std::forward<_Callable>(f), std::forward<_Args>(args)...);
}
template<typename Function, typename Tuple, size_t... I>
auto bind_N(Function &&f, Tuple &&t, std::index_sequence<I...>) {
return std::bind(f, std::get<I>(t)...);
}
template<int N, typename Function, typename Tuple>
auto bind_N(Function &&f, Tuple &&t) {
// static constexpr auto size = std::tuple_size<Tuple>::value;
return bind_N(f, t, std::make_index_sequence<N>{});
}
template<int N, typename _Callable, typename... _Args,
std::enable_if_t<!std::is_member_function_pointer_v<_Callable>, bool> = true>
auto bind_tie(_Callable &&f, _Args &&...args) {
return bind_N<N>(f, std::make_tuple(args...));
}
template<typename Function, typename _Instance, typename Tuple, size_t... I>
auto bind_N_mem(Function &&f, _Instance &&ii, Tuple &&t, std::index_sequence<I...>) {
return std::bind(f, ii, std::get<I>(t)...);
}
template<int N, typename Function, typename _Instance, typename Tuple>
auto bind_N_mem(Function &&f, _Instance &&ii, Tuple &&t) {
return bind_N_mem(f, ii, t, std::make_index_sequence<N>{});
}
template<int N, typename _Callable, typename _Instance, typename... _Args,
std::enable_if_t<std::is_member_function_pointer_v<_Callable>, bool> = true>
auto bind_tie_mem(_Callable &&f, _Instance &&ii, _Args &&...args) {
return bind_N_mem<N>(f, ii, std::make_tuple(args...));
}
template<int N, typename _Callable, typename... _Args,
std::enable_if_t<std::is_member_function_pointer_v<_Callable>, bool> = true>
auto bind_tie(_Callable &&f, _Args &&...args) {
return bind_tie_mem<N>(std::forward<_Callable>(f), std::forward<_Args>(args)...);
}
} // namespace hicc::util::cool
After the unfolding and truncation of bind_tie, we solved the problem of automatically binding placeholders, and we didn't make a big fuss, just used the most common and least complicated unfolding methods, so we are still very proud.
Now the test code can be abbreviated as this:
// connect the slots
// sig.connect(printer, _1, _2, _3, _4);
// foo ff;
// sig.on(&foo::bar, ff, _1, _2, _3, _4);
// sig.on(obj(), _1, _2, _3, _4);
sig.connect(printer);
foo ff;
sig.on(&foo::bar, ff);
sig.on(obj(), _1, _2, _3, _4);
sig.on(obj());
For static member functions, no additional tests are done, but it is the same as a normal function object, so it works correctly.
postscript
This time, the plan of Observer Pattern has unexpectedly lengthened.
But this is my original intention, and I will sort out the results of knowledge by the way, especially if it is interesting to sort them horizontally and vertically.
The complete code of this group observer mode, direct repo of Hz-common.hh and dp-observer.cc . Ignore the timeout problem that github actions often hung up.
:end:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。