Haariges Forwarding-Problem



  • Hallo!
    Ich möchte für Windows und C++ einen kleinen Thread-Wrapper konstruieren, der vorzugsweise so funktionieren soll:

    class thread;
    
    void thread_fun(const std::string&, int&&);
    int thread_fun(std::vector<unsigned>&&, float, bool);
    int thread_fun(const std::vector<unsigned>&, float, bool);
    
    std::string s = "Hello";
    std::vector<unsigned> v = { 3, 1, 8, 3, 9, 1 };
    std::shared_ptr<thread> t1 = create_thread(thread_fun, s, 5),
                            t2 = create_thread(thread_fun, { 3, 7, 2, 4 }, 3.3f, true),
                            t3 = create_thread(thread_fun, v, 2.1f, false);
    

    Die Funktion kann aussehen wie sie will und wie der Nutzer das möchte. Ich würde das mit Variadic Templates und Perfect Forwarding lösen wollen. Leider benötigt die API-Funktion für Threads einen void* als Parameter, wobei viele Typen-Informationen verloren gehen. Außerdem kann ich einen Parameter-Pack nicht in void* konvertieren, weshalb mir nun das Tupel als Forwarder einfiel.
    Meine Frage: Wie komme ich nun an die Elemente im Tupel ran (das ja verschieden groß ist), um sie der Client-Funktion im Thread zu übergeben?

    /*
    
    Lese-Reihenfolge: create_thread ---> class thread ---> thread_procedure
    ;)
    */ 
    
    // Die Funktionssignatur muss mit der API übereinstimmen
    template <typename FTy_, typename ...Args>
    unsigned __stdcall thread_procedure(void* parameters) {
       void** array = static_cast<void**>(parameters);
       FTy_ function = *static_cast<FTy_*>(array[0]);
       std::tuple<Args_...> tuple = *static_cast<std::tuple<Args...>*>(array[1]);
       // Schön wärs (wie kann ich das machen?):
       try {
          // Hier wird die Funktion aufgerufen, die der Nutzer im Thread laufen haben will
          function(std::forward(tuple.get_parameter_pack())...);
       }
       catch(...) { // Wenn die Funktion 'ne Exception werfen sollte...
          delete array[0];
          delete [] array;
          throw;
       }
       delete array[0];
       delete [] array;
       return 0u;
    }
    
    // Thread-Wrapperklasse, Ressourcenverwaltung etc.
    class thread {
    public:
       template <typename FTy_, typename ...Args_>
       friend std::shared_ptr<thread> create_thread(FTy_, Args_&&...);
    
    private:
       template <typename FTy_, typename ...Args_>
       thread(FTy_ function, Args_&&... args) {
          void** parameters = new void*[2]; // Parameter-Array
          FTy_* fun = new FTy_(function);
          parameters[0] = fun;
          std::tuple<Args_...>* tuple = new std::tuple<Args_...>(std::forward(args)...);
          parameters[1] = tuple;
          // API-Call:
          winapi::create_thread(thread_procedure<FTy_, Args_...>, parameters); 
       }
    
    };
    
    // RAII-Factory
    template <typename FTy_, typename ...Args_>
    std::shared_ptr<thread> create_thread(FTy_ function, Args_&&... args) {
       return new thread(function, std::forward(args)...);
    }
    

    Sowas wie tuple::get_parameter_pack() gibt's leider nicht. 😞



  • Also, Du setzt teilweise C++0x voraus, nur ohne C++0x Thread-Unterstützung, wie?

    Du kannst eine abstrakte Klasse bauen:

    class wrapped_job
    {
    public:
      virtual ~wrapped_job() {}
      virtual void run() = 0;
    };
    

    und dahinter den "Job" verstecken. Einen Zeiger auf so ein Objekt kann man zu void* konvertieren und so die API des Betriebssystems bedienen. In der Thread-Funktion castet man wieder zurück:

    extern "C" void my_thread_func(void* context)
    {
      try {
        static_cast<wrapped_job*>(context)->run();
      } catch (...) {
        // Mist!
      }
    }
    

    Eine konkrete Klasse könnte so aussehen:

    template<class Fun, class ParamTuple>
    class concrete_wrapped_job : public wrapped_job
    {
      Fun fun;
      ParamTuple pt;
    public:
      concrete_wrapped_job(Fun fun, ParamTuple pt)
      : fun(fun), pt(pt) {}
      void run() {
        forward_tuple_as_args(fun,pt);
      }
    };
    

    wobei ParamTuple irgendwas in der Art von std::tuple<A,B,...> für bestimmte Typen A,B,... ist. So ein Objekt mit dem Tuple zu erzeugen ist nicht so schwer:

    template<class Fun, class... Args>
    unique_ptr<wrapped_job> make_job(Fun fun, Args&&... args)
    {
      typedef decltype( make_tuple(forward<Args>(args)...) ) tuple_type;
      unique_ptr<wrapped_job> ptr (
        new concrete_wrapped_job<Fun,tuple_type>(
          fun, make_tuple(forward<Args>(args)...)
        )
      );
      return ptr;
    }
    
    :
    :
    
    void blah(int,int,double);
    
    void foo()
    {
      auto job = make_job( &blah, 2,5,3.1415 );
      job->run();
    }
    

    Der einzige Knackpunkt ist jetzt noch das forward_tuple_as_args -Template. Dazu braucht man ein bisschen Metaprogrammierung:

    template<int...> struct int_pack {};
    
    template<int Count>
    struct make_indices {
      typedef int_pack<0,1,...,Count-1> type; // <-- pseudo code (!)
    };
    
    template<class Fun, class ParamTuple, int... Idx>
    inline void weiter(Fun && fun, ParamTuple && pt, int_pack<Idx...>)
    {
      forward<Fun>(fun)(std::get<Idx>(pt)...);
    }
    
    template<class Fun, class ParamTuple>
    inline void forward_tuple_as_args(Fun && fun, ParamTuple && pt)
    {
      typedef typename remove_reference<ParamTuple>::type tuple_type;
      typedef typename make_indices<tuple_size<tuple_type>::value>::type idxpck;
      weiter(forward<Fun>(fun),forward<ParamTuple>(pt),idxpck());
    }
    

    Jetzt bleibt aber noch die Frage, wie man die Hilfsklasse make_indices richtig implementiert. Diese Variadic-Template-Übung überlasse ich Dir. :p

    BTW: Ich habe das so runtergetippt. Da sind bestimmt noch zig Fehler drin. Aber ich hoffe, dass das Prinzip klar geworden ist.

    kk



  • Hier die Lösung für make_indices:

    #include <typeinfo>
    #include <cstdlib>
    
    template<class T>
    struct id {typedef T type;};
    
    template<int... Ints>
    struct int_pack {};
    
    template<int Count, int... Tail>
    struct make_indices : make_indices<Count-1,Count-1,Tail...> {};
    template<int...Tail>
    struct make_indices<0,Tail...> : id<int_pack<Tail...> > {};
    
    int main()
    {
      typedef make_indices<6>::type T1;
      typedef int_pack<0,1,2,3,4,5> T2;
      return typeid(T1)==typeid(T2) ? EXIT_SUCCESS : EXIT_FAILURE;
    }
    


  • Sorry, hab ich lange nicht sehen lassen. 🙂
    Meine Lösung:

    #include <iostream>
    #include <cstdlib>
    #include <typeinfo>
    
    template <int...>
    struct pack { };
    
    template <int n, int... pack>
    struct indices {
    	typedef typename indices<n - 1, n - 1, pack...>::type type;
    };
    
    template <int... p>
    struct indices<0, p...> {
    	typedef pack<p...> type;
    };
    
    int main() {
    	if(typeid(indices<2>::type) == typeid(pack<0, 1>))
    		std::cout << "ID!" << std::endl;
    	std::cin.get();
    }
    

    Aber ich verstehe dein idxpck() nicht. operator() ist für int_pack ja nicht definiert. Wie gebe ich ein so ein int_pack zurück?



  • idxpack war bei mir ein Typ und kein Objekt. Mit operator() hat das nichts zu tun. Das ist die "functional syntax" zur Erzeugung eines temporären Objekts vom Typ idxpck. Der Sinn des ganzen ist es, über die Template-Argument-Deduktion wieder an die Indizes ran zukommen:

    #include <iostream>
    #include <tuple>
    
    void foo(int i,double d,char const* s)
    {
       using namespace std;
       cout << "i = " << i << '\n';
       cout << "d = " << d << '\n';
       cout << "s = " << s << '\n';
    }
    
    template<int... Ints>
    struct ipack {};
    
    template<class Tuple, int... Ints>
    void weiter(Tuple t, ipack<Ints...>)
    {
       foo(std::get<Ints>(t)...);
    }
    
    int main()
    {
       using namespace std;
       weiter( make_tuple("Hallo",42,3.1415), ipack<1,2,0>() );
    }
    

    Mit Hilfe der Template-Argument-Deduktion extrahiert der Compiler die Indizes aus dem Typ ipack<1,2,0> und stellt sie beim Funktions-Template "weiter" als Template-Parameter-Pack "Ints" zu Verfügung. Wir brauchen ein solches Parameter-Pack, damit die einzelnen Tuple-Elemente einzeln als Parameter weitergeleitet werden können. Das ipack<>-Objekt an sich ist sonst von keinem Nutzen. Deswegen benenne ich es auch gar nicht erst bei der Funktionsdefinition. Vor dem Pack-Expansionsoperator "..." steht das Muster std::get<Ints>(t). Aus dem Einsetzen der Werte von Ints im Muster und dem Aneinanderhängen wird dann
    std::get<1>(t), std::get<2>(t), std::get<0>(t)
    und foo wird dementsprechend mit drei Parametern aufgerufen.

    kk



  • Hi,
    ich frage mich, warum man eigentlich dieses Dummy-Struct benutzen muss? Wenn ich mir das so angucke sehe ich eigentlich kein Grund dagegen, die ints direkt als Parameter zu übergeben.
    Also so:

    template<class Tuple, int... Ints>
    void weiter(Tuple t, Ints...)
    {
       foo(std::get<Ints>(t)...);
    }
    

    Mir ist klar, dass es nicht funktioniert (getestet mit GCC 4.5) aber warum?
    Hast du dafür eine Erklärung Krümelkacker?

    Gruß Gate



  • Weil Ints kein Typ ist und damit nicht in der Parameterliste vorkommen kann.

    Analog (zur Verdeutlichung) ohne Ellipse:

    template<typename T> void func(T t); // T ist ein Typ. Expandiert in func<long> zu "long t"
    template<int I> void func(I i); // I ist ein Integral. Soll das in func<99> zu "99 i" expandieren???
    


  • Gate schrieb:

    Wenn ich mir das so angucke sehe ich eigentlich kein Grund dagegen, die ints direkt als Parameter zu übergeben.
    Also so:

    template<class Tuple, int... Ints>
    void weiter(Tuple t, Ints...)
    {
       foo(std::get<Ints>(t)...);
    }
    

    Mir ist klar, dass es nicht funktioniert (getestet mit GCC 4.5) aber warum?

    Ints ist hier ein "Nicht-Typ-Parameter-Pack". Das kann so nicht als Funktionsparameter auftauchen; denn Ints... steht für eine durch Komma getrennte Liste von Ganzzahlen.

    Man könnte aber folgendes machen:

    template<class Tuple, int... Ints>
    void weiter(Tuple t)
    {
       foo(std::get<Ints>(t)...);
    }
    

    und die Funktion dann so aufrufen

    weiter<my_tuple_type,0,1,2,3,4,5>(my_tuple);
    

    Doch gibt es keine Syntax, mit der man solche Template-Parameter on-the-fly erzeugen kann, so wie wir es an dieser Stelle bräuchten. Also, so etwas wie in der Art

    weiter<my_tuple_type,sequence(0,tuple_size<my_tuple_type>::value)>(my_tuple);
    

    gibt es nicht. Wenn man jetzt ein int-Parameter-Pack hätte, könnte man das an dieser Stelle einsetzen:

    weiter<my_tuple_type,Ints...>(my_tuple);
    

    Aber das hilft einem auch nicht weiter, weil sich das Problem so nur verlagert.

    Gate schrieb:

    ich frage mich, warum man eigentlich dieses Dummy-Struct benutzen muss?

    Es löst das Problem, an ein geeignetes Int-Parameter-Pack zu kommen. Der passende Typ kann über Metaprogrammierung erzeugt werden. Das Int-Parameter-Pack bekommt man dann über die Template-Argument-Deduktion. Das heißt, wenn ich einen Typen wie ipack<0,1,2,3,4,5> über Metaprogrammierung erzeugen kann, kann ich die Funktion

    template<class Tuple, int... Ints>
    void weiter(Tuple t, ipack<Ints...>)
    {
       foo(std::get<Ints>(t)...);
    }
    

    so aufrufen:

    const int N = tuple_size<my_tuple_type>::value;
    typedef mach_mir_ein_ipack<0,N>::type indices; // = ipack<0,1,2,3,4,5>
    weiter( my_tuple, indices() );
    

    Über die Regeln der Template-Argument-Deduktion kommt der Compiler selbst auf die Templateparameter, nämlich

    Tuple   = std::tuple<[i]irgendwelche Typen[/i]>
    Ints... = 0,1,2,3,4,5
    

    kk



  • Super, vielen Dank euch beiden und tolle Erklärung Krümelkacker.
    Eigentlich ist es ja völlig klar, nur habe ich da irgendwie nicht weit genug gedacht 🙂

    Gruß Gate



  • > Mit Hilfe der Template-Argument-Deduktion extrahiert der Compiler die Indizes aus dem Typ ipack<1,2,0> und stellt sie beim Funktions-Template "weiter" als Template-Parameter-Pack "Ints" zu Verfügung.

    Ah ja, es war mir noch neu, dass die TAD solche Structs in Parameter-Packs auflösen kann. Super, vielen Dank! 😉


Anmelden zum Antworten