Variadische Schnittstelle designen



  • Hi, für meine Netzwerk-Lib suche ich ein nettes Schnittstellen-Design.
    Folgendes Szenario:
    Eine Schreibe-Operation z.B. wird durch eine Klasse modeliert. Das wichtigste für sie ist zum einen der Datenpuffer, der (asynchron) geschrieben wird, zum anderen der Callback, der aufgerufen wird, sobald die Operation abgeschlossen wurde. Der Callback hat als Parameter einen unique_ptr auf genau dieses Operations-Objekt, i.e.

    auto operation = make_operation(std::move(buffer), [](auto&& operation_ptr) { ... });
    socket.async_begin(std::move(operation));
    

    Gut zu erkennen ist, das für die Zeit, in der die Operation läuft, die Ownership von Puffer und Operations-Objekt der Library übergeben wird, sodass der Caller sich nicht um Lifetimes kümmern muss.

    Der Caller kann sich zusätzliche Parameter per Capture in das Callback holen. Nur für ein Objekt geht das nicht, den Socket:

    auto operation = make_operation(std::move(buffer), [socket = std::move(socket)](auto&& operation_ptr) { ... });
    socket.async_begin(std::move(operation)); // socket jetzt leider ungültig
    

    Meine Lösung bisher war also, dass eine Operation intern variadisch viele zusätzliche Parameter hält (als std::tuple), die sie dann im Callback als weitere Parameter dem Nutzer zurückgibt:

    auto operation = make_operation(std::move(buffer), [](auto&& operation_ptr, auto&& socket, int foo) { ... }, std::move(socket), 23);
    auto& sock_ref = operation->get_parameter<0>(); // erhalte Referenz auf das 0-te Objekt, hier also der Socket
    sock_ref.async_begin(std::move(operation)); // Starte Operation
    

    Ist zwar etwas umständlich zu implementieren, aber funktioniert wie gewünscht. Leider ist mir kein Weg bekannt, eine (mutable) Referenz auf Member eines Lambdas zu bekommen?

    So, mein Problem beginnt jetzt: make_operation ist eine variadische Funktion, deren optionale Parameter nutzerbestimmt sind. Allerdings brauche ich auch optionale Parameter für die Operation selbst, z.B. wenn ich optionale Flags oder mehrere Buffer übergeben will:

    auto operation = make_operation(move(buffer), callback, 1, 3.53, "hello!");
    operation = make_operation(my_flags, move(buffer), callback, 1, 3.53, "hello!");
    operation = make_operation(my_flags, move(buffer), move(more_buffer), callback, 1, 3.53, "hello!");
    

    Bzw. soll es auch Operationen geben, wo es überhaupt optional ist, einen Puffer zu übergeben.
    Die Funktionen oben müsste ich für den Zweck also überladen, aber dann komme ich in eine SFINAE-Hölle (alle Parameter von make_operation haben Template-Typen, insbesondere der Callback und die Buffer). Ich muss irgendwie Puffer-Typen von Callback-Typen und den Variadischen Typen des Nutzers unterscheiden können. Erst dachte ich an so etwas wie ein enable_if<> mit is_callable<>, um den ersten Typen, der die Bedingung erfüllt, als den Callback zu identifizieren und alles danach kommende als Variadische Typen. Das scheitert aber genau dann, wenn der Fall eintritt, indem ein Buffer-Typ zufällig auch genau die is_callable<>-Bedingung erfüllt.

    Andererseits kann ich auch versuchen, die Buffer-Typen zu identifizieren. Die Library muss an irgendeiner Stelle jedes Puffer-Objekt in ein Array-Pointer/Size-Paar runterbrechen. Wenn der Nutzer exotische Typen benutzt, muss er irgendwo angeben, wie die Library das machen soll, dann könnte ich sowas wie enable_if mit is_convertible versuchen.
    Die Funktionsdeklaration sieht ungefähr so aus (bisher):

    template <typename BufferType, typename OperationType, typename ...Params>
    decltype(auto) make_operation(BufferType&&, OperationType&&, Params&&...);
    


  • Hrmmm, Du könntest Dir ja einen explizit catchenden Typen definieren, sodass "operation" das nicht für Dich übernehmen muss, dann bleiben alle variadischen Parameter für "operation" selber übrig. Irgendwie so (ist jetzt auf die schnelle):

    template< typename Callable, typename... Args >
    struct explicit_catch_struct
    {
    	using func_type = std::function<void(Args...)>;
    
    	template< typename... Args2 >
    	explicit_catch_struct(Callable func_, Args2&&... args2)
    		: func(std::move(func_)), args(std::forward<Args2>(args2)...)
    	{}
    
    	using index_sequence = std::make_index_sequence<sizeof...(Args)>;
    	using tuple_type = std::tuple<Args...>;
    
    	explicit_catch_struct(explicit_catch_struct&&) = default;
    	explicit_catch_struct& operator=(explicit_catch_struct&&) = default;
    
    	template< size_t... Indices, typename... OpArgs >
    	void do_operator(std::index_sequence<Indices...>, OpArgs&&... op_args)
    	{
    		func(std::forward<OpArgs>(op_args)..., std::move(std::get<Indices>(args))...);
    	}
    
    	template<typename... OpArgs>
    	void operator()(OpArgs&&... op_args) {
    		do_operator(index_sequence(), std::forward<OpArgs>(op_args)...);
    	}
    
    	template<size_t Index>
    	std::tuple_element_t<Index, tuple_type>& get()
    	{
    		return args.get<Index>();
    	}
    
    	template<size_t Index>
    	std::tuple_element_t<Index, tuple_type>& get() const
    	{
    		return args.get<Index>();
    	}
    
    	Callable func;
    	tuple_type args;
    };
    
    template< typename Callable, typename... Args >
    explicit_catch_struct<Callable, Args...> explicit_catch(Callable call, Args&&... args)
    {
    	return explicit_catch_struct<Callable, Args...>(std::move(call), std::forward<Args>(args)...);
    }
    
    void func()
    {
    	auto sas = explicit_catch([](int, int) {}, 5);
    	sas(5);
    }
    

Log in to reply