2015-01-27 16 views
5

Tôi đang cố gắng thực hiện 'nhiệm vụ' theo kiểu std::async và lưu trữ nó trong một thùng chứa. Tôi phải nhảy qua hoops để đạt được nó, nhưng tôi nghĩ rằng phải có một cách tốt hơn.Tôi làm cách nào để lưu trữ gói đóng gói chung trong một vùng chứa?

std::vector<std::function<void()>> mTasks; 

template<class F, class... Args> 
std::future<typename std::result_of<typename std::decay<F>::type(typename std::decay<Args>::type...)>::type> 
push(F&& f, Args&&... args) 
{ 
    auto func = std::make_shared<std::packaged_task<typename std::result_of<typename std::decay<F>::type(typename std::decay<Args>::type...)>::type()>>(std::bind(std::forward<F>(f), std::forward<Args>(args)...)); 
    auto future = func->get_future(); 

    // for some reason I get a compilation error in clang if I get rid of the `=, ` in this capture: 
    mTasks.push_back([=, func = std::move(func)]{ (*func)(); }); 

    return future; 
} 

Vì vậy, tôi đang sử dụng bind ->packaged_task ->shared_ptr ->lambda ->function. Làm thế nào tôi có thể làm điều này tốt hơn/tối ưu hơn? Nó chắc chắn sẽ dễ dàng hơn nếu có một số std::function có thể thực hiện nhiệm vụ không thể sao chép được nhưng có thể di chuyển được. Tôi có thể std :: forward args vào chụp một lambda, hoặc tôi phải sử dụng bind?

+0

Tại sao bạn không đặt tác vụ đóng gói trực tiếp vào vectơ? –

+1

@KerrekSB Vì 'packaged_task' không thể sao chép được và' std :: function' chỉ chấp nhận những thứ có thể sao chép được. – David

+1

Tôi có nghĩa là 'std :: vector >'. –

Trả lời

6

Không có sự giết người nào như quá mức cần thiết.

Bước 1: viết một SFINAE thân thiện std::result_of và một chức năng để giúp kêu gọi thông qua tuple:

namespace details { 
    template<size_t...Is, class F, class... Args> 
    auto invoke_tuple(std::index_sequence<Is...>, F&& f, std::tuple<Args>&& args) 
    { 
    return std::forward<F>(f)(std::get<Is>(std::move(args))); 
    } 
    // SFINAE friendly result_of: 
    template<class Invocation, class=void> 
    struct invoke_result {}; 
    template<class T, class...Args> 
    struct invoke_result<T(Args...), decltype(void(std::declval<T>()(std::declval<Args>()...))) > { 
    using type = decltype(std::declval<T>()(std::declval<Args>()...)); 
    }; 
    template<class Invocation, class=void> 
    struct can_invoke:std::false_type{}; 
    template<class Invocation> 
    struct can_invoke<Invocation, decltype(void(std::declval< 
    typename invoke_result<Inocation>::type 
    >()))>:std::true_type{}; 
} 

template<class F, class... Args> 
auto invoke_tuple(F&& f, std::tuple<Args>&& args) 
{ 
    return details::invoke_tuple(std::index_sequence_for<Args...>{}, std::forward<F>(f), std::move(args)); 
} 

// SFINAE friendly result_of: 
template<class Invocation> 
struct invoke_result:details::invoke_result<Invocation>{}; 
template<class Invocation> 
using invoke_result_t = typename invoke_result<Invocation>::type; 
template<class Invocation> 
struct can_invoke:details::can_invoke<Invocation>{}; 

Bây giờ chúng ta có invoke_result_t<A(B,C)> mà là một SFINAE thân thiện result_of_t<A(B,C)>can_invoke<A(B,C)> mà chỉ làm việc kiểm tra.

Tiếp theo, hãy viết một move_only_function, một phiên bản di chuyển chỉ của std::function:

namespace details { 
    template<class Sig> 
    struct mof_internal; 
    template<class R, class...Args> 
    struct mof_internal { 
    virtual ~mof_internal() {}; 
    // 4 overloads, because I'm insane: 
    virtual R invoke(Args&&... args) const& = 0; 
    virtual R invoke(Args&&... args) & = 0; 
    virtual R invoke(Args&&... args) const&& = 0; 
    virtual R invoke(Args&&... args) && = 0; 
    }; 

    template<class F, class Sig> 
    struct mof_pimpl; 
    template<class R, class...Args, class F> 
    struct mof_pimpl<F, R(Args...)>:mof_internal<R(Args...)> { 
    F f; 
    virtual R invoke(Args&&... args) const& override { return f(std::forward<Args>(args)...); } 
    virtual R invoke(Args&&... args)  & override { return f(std::forward<Args>(args)...); } 
    virtual R invoke(Args&&... args) const&& override { return std::move(f)(std::forward<Args>(args)...); } 
    virtual R invoke(Args&&... args)  && override { return std::move(f)(std::forward<Args>(args)...); } 
    }; 
} 

template<class R, class...Args> 
struct move_only_function<R(Args)> { 
    move_only_function(move_only_function const&)=delete; 
    move_only_function(move_only_function &&)=default; 
    move_only_function(std::nullptr_t):move_only_function() {} 
    move_only_function() = default; 
    explicit operator bool() const { return pImpl; } 
    bool operator!() const { return !*this; } 
    R operator()(Args...args)  & { return     pImpl().invoke(std::forward<Args>(args)...); } 
    R operator()(Args...args)const& { return     pImpl().invoke(std::forward<Args>(args)...); } 
    R operator()(Args...args)  &&{ return std::move(*this).pImpl().invoke(std::forward<Args>(args)...); } 
    R operator()(Args...args)const&&{ return std::move(*this).pImpl().invoke(std::forward<Args>(args)...); } 

    template<class F,class=std::enable_if_t<can_invoke<decay_t<F>(Args...)>> 
    move_only_function(F&& f): 
    m_pImpl(std::make_unique<details::mof_pimpl<std::decay_t<F>, R(Args...)>>(std::forward<F>(f))) 
    {} 
private: 
    using internal = details::mof_internal<R(Args...)>; 
    std::unique_ptr<internal> m_pImpl; 

    // rvalue helpers: 
    internal  & pImpl()  & { return *m_pImpl.get(); } 
    internal const& pImpl() const& { return *m_pImpl.get(); } 
    internal  && pImpl()  && { return std::move(*m_pImpl.get()); } 
    internal const&& pImpl() const&& { return std::move(*m_pImpl.get()); } // mostly useless 
}; 

không dự thi, chỉ phun mã. can_invoke cung cấp cho hàm tạo SFINAE cơ bản - bạn có thể thêm "kiểu trả về chuyển đổi đúng" và "loại trả về void có nghĩa là chúng tôi bỏ qua trả lại" nếu bạn muốn.

Bây giờ chúng tôi sẽ làm lại mã của bạn. Thứ nhất, nhiệm vụ của bạn là chức năng di chuyển chỉ, không phải chức năng:

std::vector<move_only_function<X>> mTasks; 

Tiếp theo, chúng tôi lưu trữ các tính R loại một lần, và sử dụng nó một lần nữa:

template<class F, class... Args, class R=std::result_of_t<std::decay<F>_&&(std::decay_t<Args>&&...)>> 
std::future<R> 
push(F&& f, Args&&... args) 
{ 
    auto tuple_args=std::make_tuple(std::forward<Args>(args)...)]; 

    // lambda will only be called once: 
    std::packaged_task<R()> task([f=std::forward<F>(f),args=std::move(tuple_args)] 
    return invoke_tuple(std::move(f), std::move(args)); 
    }); 

    auto future = func.get_future(); 

    // for some reason I get a compilation error in clang if I get rid of the `=, ` in this capture: 
    mTasks.emplace_back(std::move(task)); 

    return future; 
} 

chúng ta nhét các đối số vào một tuple, chuyển tuple đó vào lambda và gọi tuple theo kiểu "chỉ làm điều này một lần" trong lambda. Vì chúng ta sẽ chỉ gọi hàm một lần, chúng ta tối ưu hóa lambda cho trường hợp đó.

A packaged_task<R()> tương thích với move_only_function<R()> không giống như std::function<R()>, vì vậy chúng tôi chỉ có thể di chuyển nó vào vectơ của chúng tôi. Các std::future chúng tôi nhận được từ nó sẽ làm việc tốt mặc dù chúng tôi đã nhận nó trước khi move.

Điều này sẽ giảm chi phí của bạn một chút. Tất nhiên, có rất nhiều boilerplate.

Tôi chưa biên soạn bất kỳ mã nào ở trên, tôi đã giải thích nó, vì vậy tỷ lệ cược tất cả các biên dịch đều thấp. Nhưng các lỗi chủ yếu là tpyos.

Ngẫu nhiên, tôi quyết định cung cấp cho move_only_function 4 quá tải khác nhau () (rvalue/lvalue và const/not). Tôi có thể đã thêm dễ bay hơi, nhưng điều đó có vẻ thiếu thận trọng. Mà tăng boilerplate, thừa nhận.

Ngoài ra, move_only_function của tôi thiếu thao tác "lấy tại công cụ được lưu trữ cơ bản" mà std::function có. Hãy gõ xóa rằng nếu bạn thích.Và nó xử lý (R(*)(Args...))0 như thể nó là một con trỏ hàm thực (Tôi trở true khi đúc để bool, không giống như null: loại chô bôi của convert-to- bool có thể đáng giá cho việc thực hiện công nghiệp chất lượng hơn

Tôi viết lại std::function. vì std thiếu std::move_only_function, và khái niệm nói chung là một trong những hữu ích (được minh chứng bởi packaged_task). giải pháp của bạn làm cho di chuyển callable của bạn bằng cách gói nó với một std::shared_ptr.

Nếu bạn không thích những soạn sẵn ở trên, xem xét viết make_copyable(F&&), có đối tượng hàm F và kết thúc tốt đẹp bằng cách sử dụng kỹ thuật shared_ptr của bạn để làm cho nó có thể sao chép được. Bạn thậm chí có thể thêm SFINAE để tránh làm điều đó nếu nó đã được sao chép (và gọi nó là ensure_copyable).

Sau đó, mã ban đầu của bạn sẽ sạch hơn, vì bạn chỉ cần tạo packaged_task có thể sao chép, sau đó lưu trữ mã đó.

template<class F> 
auto make_function_copyable(F&& f) { 
    auto sp = std::make_shared<std::decay_t<F>>(std::forward<F>(f)); 
    return [sp](auto&&...args){return (*sp)(std::forward<decltype(args)>(args)...); } 
} 
template<class F, class... Args, class R=std::result_of_t<std::decay<F>_&&(std::decay_t<Args>&&...)>> 
std::future<R> 
push(F&& f, Args&&... args) 
{ 
    auto tuple_args=std::make_tuple(std::forward<Args>(args)...)]; 

    // lambda will only be called once: 
    std::packaged_task<R()> task([f=std::forward<F>(f),args=std::move(tuple_args)] 
    return invoke_tuple(std::move(f), std::move(args)); 
    }); 

    auto future = func.get_future(); 

    // for some reason I get a compilation error in clang if I get rid of the `=, ` in this capture: 
    mTasks.emplace_back(make_function_copyable(std::move(task))); 

    return future; 
} 

yêu cầu này vẫn yêu cầu invoke_tuple boilerplate ở trên, chủ yếu là vì tôi không thích bind.

+0

Vì vậy, 2 vấn đề gốc là không có 'move_only_function' và bạn không thể chuyển tiếp các đối số variadic vào một capture lambda. Bạn đã viết một 'move_only_function' và chuyển tiếp các đối số variadic vào một bản ghi lambda bằng cách sử dụng một' tuple'. touché. Nhưng, là điều 'tuple' nào tối ưu hơn/tốt hơn so với việc sử dụng' bind', bởi vì 'bind' khá là hơi ít. Ngoài ra, chỉ có lớp tiêu đề đơn giản của tôi, với tất cả những thứ này; C++ nên hỗ trợ cả hai vấn đề! – David

+0

@Dave yep. Một điều thú vị là 'move_only_function' (hoặc bất kỳ thứ gì bạn muốn gọi nó) là hữu ích cho các mục đích khác, và bạn có thể lấy các 'đại biểu có thể nhanh nhất' để làm cho nó nhanh hơn hầu hết các triển khai' std :: function'. Và vâng, 'bind' làm một cái gì đó khá giống với điệu nhảy' tuple' của tôi, nhưng tôi không thích 'bind' đủ (nó có quá nhiều ma thuật - ràng buộc một ràng buộc và xé tóc ra), và' invoke_tuple' đủ đơn giản (6-10 dòng mã?) Mà nó cảm thấy sạch hơn. – Yakk

+0

Đi xuống suy nghĩ để sử dụng 'bind' và giảm bớt một số boilerplate (nhưng vẫn sử dụng' move_only_function', nó sẽ là: 'mTasks.push_back (std :: bind (std :: move (func), std :: forward (args) ...)); 'nếu tôi tạo' packaged_task' 'R (args ...)' thay vì 'R()'. – David

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