2014-09-07 13 views
6

Tôi muốn viết trình quản lý sự kiện hỗ trợ truyền số lượng đối số tùy ý. Để hiển thị cho bạn biểu mẫu, đây là một ví dụ. Xin lưu ý rằng một mục tiêu là không cần định nghĩa lớp cho mọi sự kiện. Thay vào đó, các sự kiện được biểu diễn bằng các tên chuỗi. Đầu tiên, cho phép đăng ký bốn người nghe đến cùng một sự kiện. Chúng khác nhau về số lượng tham số mà chúng chấp nhận.Có thể thực hiện trình quản lý sự kiện lambda này trong C++ không?

Events events; 

events.listen("key", [=] { 
    cout << "Pressed a key." << endl; 
}); 

events.listen("key", [=](int code) { 
    cout << "Pressed key with code " << code << "." << endl; 
}); 

events.listen("key", [=](int code, string user) { 
    cout << user << " pressed key with code " << code << "." << endl; 
}); 

events.listen("key", [=](int code, string user, float duration) { 
    cout << user << " pressed key with code " << code << " for " << duration 
     << " seconds." << endl; 
}); 

events.listen("key", [=](string user) { 
    cout << user << " pressed a key." << endl; 
}); 

Bây giờ hãy kích hoạt sự kiện bằng một số đối số. events.fire("key", {42, "John"}); Điều này nên gọi lambdas đã đăng ký phù hợp với một số hoặc tất cả các đối số. Ví dụ: cuộc gọi này sẽ tạo ra kết quả sau cho năm người nghe mà chúng tôi đã đăng ký.

  1. In "Đã nhấn một phím".
  2. In "Phím được bấm với mã 42."
  3. In "John nhấn phím với mã 42."
  4. Ném ngoại lệ vì người nghe không khớp với chữ ký.
  5. Ném ngoại lệ vì người nghe không khớp với chữ ký.

Có thể đạt được hành vi này trong C++ không? Nếu vậy, làm cách nào tôi có thể lưu trữ các callbacks khác nhau trong bộ sưu tập trong khi vẫn có thể truyền lại chúng để gọi với các số tham số khác nhau? Tôi nghĩ rằng nhiệm vụ này không phải là dễ dàng vì vậy mọi gợi ý giúp.

+1

Đ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

+0

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

+0

@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

Trả lời

4

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ế:

  1. Loại đối số phải được sao chép;
  2. Các đối số luôn được sao chép, không bao giờ được di chuyển;
  3. 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);
  4. 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.

+0

Cảm ơn bạn rất nhiều. Tôi gặp một số rắc rối khi sử dụng 'integer_sequence'. Bạn có biết phiên bản gcc nào hỗ trợ phiên bản này không? – danijar

+0

@danijar: 4,9 Tôi đoán, nhưng nó là khá dễ dàng để thi đua. Kỹ thuật này thường được gọi là "lừa chỉ mục". Đây là một ví dụ đơn giản (http://coliru.stacked-crooked.com/a/8d338cf35f329ca0). –

+0

Tuyệt vời! Tôi vừa thêm một đối số mẫu bổ sung để tuân thủ cú pháp chuẩn. – danijar

3

một mục tiêu là không cần định nghĩa lớp cho mọi sự kiện.

Đó là dấu hiệu tốt cho thấy bạn muốn cái gì khác hơn C++ cho mục đích của bạn vì nó không có khả năng phản chiếu động. (Nếu bạn làm hãy sử dụng thứ gì đó năng động hơn nhưng vẫn cần phải giao tiếp với C++, bạn cần phải thu hẹp khoảng cách đó, vì vậy câu trả lời này có thể hoặc vẫn không hữu ích cho điều đó.)

Bây giờ trong khi có thể để xây dựng một hệ thống động (hạn chế), bạn nên tự hỏi mình xem đó có phải là điều bạn thực sự muốn làm hay không. Ví dụ. nếu bạn đóng trên thế giới của các sự kiện và chữ ký callback của họ, bạn sẽ giữ lại rất nhiều loại an toàn:

// assumes variant type, e.g. Boost.Variant 
using key_callback = variant< 
    function<void(int)>     // code 
    , function<void(int, string)>  // code, user 
    , function<void(int, string, float)> // code, user, duration 
    , function<void(string)>    // user 
>; 

using callback_type = variant<key_callback, …more event callbacks…>; 

Với tinh thần gắn bó với yêu cầu của bạn tuy nhiên, dưới đây là cách để lưu trữ bất kỳ callback †, và vẫn có thể gọi nó:

using any = boost::any; 
using arg_type = std::vector<any>; 

struct bad_signature: std::exception {}; 
struct bad_arity: bad_signature {}; 
struct bad_argument: bad_signature { 
    explicit bad_argument(int which): which{which} {} 
    int which; 
}; 

template<typename Callable, typename Indices, typename... Args> 
struct erased_callback; 

template<typename Callable, std::size_t... Indices, typename... Args> 
struct erased_callback<Callable, std::index_sequence<Indices...>, Args...> { 
    // you can provide more overloads for cv/ref quals 
    void operator()(arg_type args) 
    { 
     // you can choose to be lax by using < 
     if(args.size() != sizeof...(Args)) { 
      throw bad_arity {}; 
     } 

     callable(restore<Args>(args[Indices], Indices)...); 
    } 

    Callable callable; 

private: 
    template<typename Arg> 
    static Arg&& restore(any& arg, int index) 
    { 
     using stored_type = std::decay_t<Arg>; 
     if(auto p = boost::any_cast<stored_type>(&arg)) { 
      return std::forward<Arg>(*p); 
     } else { 
      throw bad_argument { index }; 
     } 
    } 
}; 

template< 
    typename... Args, typename Callable 
    , typename I = std::make_index_sequence<sizeof...(Args)> 
> 
erased_callback<std::decay_t<Callable>, I, Args...> erase(Callback&& callback) 
{ return { std::forward<Callback>(callback) }; } 

// in turn we can erase an erased_callback: 
using callback_type = std::function<void(arg_type)>; 

/* 
* E.g.: 
* callback_type f = erase<int>([captures](int code) { ... }); 
*/ 

Coliru demo.

Nếu bạn có đặc điểm kiểu có thể đoán chữ ký của một loại có thể gọi, bạn có thể viết erase sử dụng nó (trong khi vẫn cho phép người dùng điền vào cho những trường hợp không thể suy ra). Tôi không sử dụng một cái trong ví dụ vì đó là một loại giun khác.

†: 'bất kỳ' có nghĩa là bất kỳ đối tượng callable chấp nhận một số con số của đối số copyable, trở void -bạn có thể thư giãn các yêu cầu về lập luận bằng cách sử dụng một động thái chỉ bao bọc tương tự như boost::any

Các vấn đề liên quan