Tôi đồng ý với quan điểm Luc rằng một cách tiếp cận kiểu an toàn có lẽ phù hợp hơn, nhưng giải pháp sau đây làm nhiều hơn hoặc ít hơn những gì bạn muốn, với một vài hạn chế:
- Loại đối số phải được sao chép;
- Các đối số luôn được sao chép, không bao giờ được di chuyển;
- Trình xử lý có tham số N được gọi nếu và chỉ khi các loại đối số N đầu tiên
fire()
khớp chính xác chính xác các loại tham số của trình xử lý, không thực hiện chuyển đổi tiềm ẩn (ví dụ: từ chuỗi ký tự sang std::string
);
- Trình xử lý không thể là functors có nhiều hơn một quá tải
operator()
.
Đây là những gì tôi, giải pháp cuối cùng cho phép bạn viết:
void my_handler(int x, const char* c, double d)
{
std::cout << "Got a " << x << " and a " << c
<< " as well as a " << d << std::endl;
}
int main()
{
event_dispatcher events;
events.listen("key",
[] (int x)
{ std::cout << "Got a " << x << std::endl; });
events.listen("key",
[] (int x, std::string const& s)
{ std::cout << "Got a " << x << " and a " << s << std::endl; });
events.listen("key",
[] (int x, std::string const& s, double d)
{ std::cout << "Got a " << x << " and a " << s
<< " as well as a " << d << std::endl; });
events.listen("key",
[] (int x, double d)
{ std::cout << "Got a " << x << " and a " << d << std::endl; });
events.listen("key", my_handler);
events.fire("key", 42, std::string{"hi"});
events.fire("key", 42, std::string{"hi"}, 3.14);
}
Cuộc gọi đầu tiên fire()
sẽ cho kết quả sau:
Got a 42
Got a 42 and a hi
Bad arity!
Bad argument!
Bad arity!
Trong khi cuộc gọi thứ hai sẽ tạo ra những điều sau đây đầu ra:
Got a 42
Got a 42 and a hi
Got a 42 and a hi as well as a 3.14
Bad argument!
Bad argument!
Đây là live example.
Việc triển khai dựa trên boost::any
. Trái tim của nó là dispatcher
functor. Toán tử cuộc gọi của nó lấy một vectơ các đối số loại đã xóa và gửi chúng đến đối tượng có thể gọi được mà nó được xây dựng (trình xử lý của bạn). Nếu loại đối số không khớp, hoặc nếu trình xử lý chấp nhận nhiều đối số hơn cung cấp, nó chỉ in lỗi đến đầu ra tiêu chuẩn, nhưng bạn có thể làm cho nó ném nếu bạn muốn hoặc làm bất cứ điều gì bạn thích:
template<typename... Args>
struct dispatcher
{
template<typename F> dispatcher(F f) : _f(std::move(f)) { }
void operator() (std::vector<boost::any> const& v)
{
if (v.size() < sizeof...(Args))
{
std::cout << "Bad arity!" << std::endl; // Throw if you prefer
return;
}
do_call(v, std::make_integer_sequence<int, sizeof...(Args)>());
}
private:
template<int... Is>
void do_call(std::vector<boost::any> const& v, std::integer_sequence<int, Is...>)
{
try
{
return _f((get_ith<Args>(v, Is))...);
}
catch (boost::bad_any_cast const&)
{
std::cout << "Bad argument!" << std::endl; // Throw if you prefer
}
}
template<typename T> T get_ith(std::vector<boost::any> const& v, int i)
{
return boost::any_cast<T>(v[i]);
}
private:
std::function<void(Args...)> _f;
};
Sau đó, có một vài tiện ích để tạo điều vận ra khỏi một functor handler (có một tiện ích tương tự để tạo điều vận ra khỏi con trỏ chức năng):
template<typename T>
struct dispatcher_maker;
template<typename... Args>
struct dispatcher_maker<std::tuple<Args...>>
{
template<typename F>
dispatcher_type make(F&& f)
{
return dispatcher<Args...>{std::forward<F>(f)};
}
};
template<typename F>
std::function<void(std::vector<boost::any> const&)> make_dispatcher(F&& f)
{
using f_type = decltype(&F::operator());
using args_type = typename function_traits<f_type>::args_type;
return dispatcher_maker<args_type>{}.make(std::forward<F>(f));
}
các function_traits
helper là một đặc điểm đơn giản để tìm ra các loại của trình xử lý để chúng tôi có thể chuyển chúng làm đối số mẫu cho dispatcher
:
template<typename T>
struct function_traits;
template<typename R, typename C, typename... Args>
struct function_traits<R(C::*)(Args...)>
{
using args_type = std::tuple<Args...>;
};
template<typename R, typename C, typename... Args>
struct function_traits<R(C::*)(Args...) const>
{
using args_type = std::tuple<Args...>;
};
Rõ ràng toàn bộ điều này sẽ không hoạt động nếu trình xử lý của bạn là một hàm với một số toán tử cuộc gọi quá tải, nhưng hy vọng giới hạn này sẽ không quá nghiêm trọng đối với bạn.
Cuối cùng, lớp event_dispatcher
cho phép bạn lưu trữ xử lý kiểu xóa trong một Multimap bằng cách gọi listen()
, và gọi chúng khi bạn gọi fire()
với phím thích hợp và các đối số thích hợp (đối tượng events
của bạn sẽ được một thể hiện của lớp này) :
struct event_dispatcher
{
public:
template<typename F>
void listen(std::string const& event, F&& f)
{
_callbacks.emplace(event, make_dispatcher(std::forward<F>(f)));
}
template<typename... Args>
void fire(std::string const& event, Args const&... args)
{
auto rng = _callbacks.equal_range(event);
for (auto it = rng.first; it != rng.second; ++it)
{
call(it->second, args...);
}
}
private:
template<typename F, typename... Args>
void call(F const& f, Args const&... args)
{
std::vector<boost::any> v{args...};
f(v);
}
private:
std::multimap<std::string, dispatcher_type> _callbacks;
};
Một lần nữa, toàn bộ mã có sẵn here.
Điều này là hoàn toàn có thể, và tôi thực sự có một thực hiện rất tốt đẹp này với một vài khác biệt nhỏ. Tôi có một vài gợi ý có thể có liên quan: 1) Strings tạo ra các khóa rất nghèo, vì vậy trừ khi bạn có một lý do rất cụ thể để tạo chuỗi khóa, tôi thực sự gợi ý gắn bó với một số loại thẻ. Lợi ích nói chung là hơi ít chi phí, nhưng đầu tiên và quan trọng nhất loại kiểm tra và đánh bắt lỗi chính tả. 2) Hầu hết các lỗi logic ở đây sẽ bị bắt tại thời gian chạy bởi các ngoại lệ, tôi không hoàn toàn thấy được lợi ích của việc có bất kỳ lời gọi nào của lệnh gọi tín hiệu. – Ylisar
Dù sao, tôi có thể cho bạn thấy triển khai của tôi đôi khi vào ngày mai khi tôi ở văn phòng, nó linh hoạt và hỗ trợ ánh xạ lambdas với chữ ký khác nhau cho tín hiệu nhưng sử dụng thẻ tĩnh để gửi. – Ylisar
@Ylisar Điều đó thật tuyệt! Tất nhiên sự không khớp chữ ký chỉ có thể được phát hiện khi sự kiện này thực sự bị sa thải, tôi biết điều đó. – danijar