[C++] Variadic template function pointer type deduction

Pagina: 1
Acties:

Vraag


Acties:
  • 0 Henk 'm!

  • cyberstalker
  • Registratie: September 2005
  • Niet online

cyberstalker

Eersteklas beunhaas

Topicstarter
Ik probeer een variadic template function pointer te gebruiken. Doel hiervan is om een C++ library te wrappen waarbij er via het template callback functions gegenereerd worden die uiteindelijk member functions aanroepen op een class. Het opzoeken van 'this' is hierbij geen probleem.

De onderliggende C-library werkt met een struct met function pointers die gezet kunnen worden. Een voorbeeld:

C++:
1
2
3
4
5
struct callbacks {
    int(*callback1)(const char*);
    int(*callback2)(float);
    void(*callback2)(const char*, const char*);
};


Nu wil ik dus graag een class maken die deze callbacks implementeert. Dat gaat natuurlijk niet een op een direct goed omdat een member function een this-pointer nodig heeft en dus een andere signatuur krijgt. Het ophalen van de instantie is geen enkel probleem, maar aangezien het om nogal wat callbacks gaat (met allemaal een andere signatuur) wil ik liever niet voor elke functie zelf een wrapper schrijven. Tijd voor templates dus.

Een wrapper template voor een specifieke signatuur is zo gemaakt:

C++:
1
2
3
4
5
6
template <int(MyClass::*CALLBACK)(const char*)>
static int wrapper(const char *input)
{
    auto *instance = getInstance();
    return (instance::*callback)(input);
}


Deze werkt prima voor alle callbacks met die signatuur, maar op die manier zou ik het voor alle verschillende signatures moeten gaan implementeren. Dat moet beter kunnen. De volgende manier werkt i.i.g. niet:

C++:
1
2
3
4
5
6
template <typename RESULT, typename ...PARAMETERS, RESULT(MyClass::*callbacks)(parameters...)>
static result wrapper(PARAMETERS ...parameters)
{
    auto *instance = getInstance();
    return (instance::*CALLBACK)(std::forward<Parameters...>(parameters)...);
}


Dit compileert niet omdat een variadic template altijd aan het eind moet staan. Dat kan echter niet omdat we deze nodig hebben voor de function pointer. Daarnaast - als het wel zou werken - kun je type deduction dan natuurlijk wel vergeten.

Het enige dat wel werkt is het volgende:

C++:
1
2
3
4
5
6
7
8
9
10
11
12
template <class T> class Callback {};

template <class T, class ...Arguments>
struct Callback<T(Arguments...)>
{
    template<T(MyClass::*CALLBACK)(Arguments...)
    static wrap(Arguments... parameters)
    {
        auto *instance = getInstance();
        return (instance::*CALLBACK)(std::forward<Arguments>(parameters)...);
    }
};


Dit is toch niet helemaal wat ik wil aangezien ik het gevoel heb dat het mogelijk moet zijn om het type van de callback te deducen. Ik krijg het echter totaal niet voor elkaar. Zonder struct lukt het me helemaal niet wegens het kip-ei probleem van het variadic template dat aan het einde moet maar niet kan omdat ik deze nodig heb voor de function pointer. Via het struct wordt het weer erg onduidelijk.

Is er een manier om een template zo te schrijven dat het type gewoon deduced kan worden?

Ik ontken het bestaan van IE.

Alle reacties


Acties:
  • +1 Henk 'm!

  • LeX-333
  • Registratie: Maart 2004
  • Laatst online: 21-11-2016
Dit is mogelijk. Een vervelend probleem is dat er een relatie gemaakt moet worden tussen een C functie pointer en een C++ functie. Sommige C interfaces bieden een opaque pointer aan in callbacks zodat je een this pointer mee kunt geven, dat is in jouw voorbeeld niet het geval. Daarom moet het met static variabelen, wat het iets gecompliceerder maakt.

Ik heb het voorbeeld iets aangepast:
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
struct callbacks
{
    int(*callback1a)(const char *);
    int(*callback1b)(const char *);
    int(*callback2)(float);
    void(*callback3)(const char *, const char *);
};

class my_class
{
public:
    my_class() { instance = this; }
    ~my_class() { instance = nullptr; }

    int function1a(const char *a) { std::cout << "function1a: " << a << std::endl; return 1; }
    int function1b(const char *a) { std::cout << "function1b: " << a << std::endl; return 2; }
    int function2(float a) { std::cout << "function2: " << a << std::endl; return 3; }
    void function3(const char *a, const char *b) { std::cout << "function3: " << a << ", " << b << std::endl; }

    static my_class * get_instance() { return instance; }

private:
    static my_class *instance;
};

my_class * my_class::instance = nullptr;


Onderstaande wrapper klasse kan nu de functies genereren:
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
template <unsigned Id>
class wrapper
{
private:
    template <class Obj, class Ret, class ...Args>
    struct wrap
    {
        static Ret call(Args... args)
        {
            return (Obj::get_instance()->*fn)(std::forward<Args>(args)...);
        }

        static Ret(Obj::* fn)(Args...);
    };

public:
    template <class Obj, class Ret, class ...Args>
    static Ret(* bind(Ret(Obj::*fn)(Args...)))(Args...)
    {
        wrap<Obj, Ret, Args...>::fn = fn;

        return &wrap<Obj, Ret, Args...>::call;
    }
};

template <unsigned Id>
template <class Obj, class Ret, class ...Args>
Ret(Obj::* wrapper<Id>::wrap<Obj, Ret, Args...>::fn)(Args...) = nullptr;


Regel 18 is een functie genaamd "bind" die een functie pointer returned van het type "Ret(*)(Args...)" en een pointer-to-member-function argument heeft van het type "Ret(Obj::*)(Args...)".

En hier een voorbeeldje hoe het te gebruiken:
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main()
{
    class my_class my_class;

    struct callbacks callbacks;
    callbacks.callback1a = wrapper<0>::bind(&my_class::function1a);
    callbacks.callback1b = wrapper<1>::bind(&my_class::function1b);
    callbacks.callback2 = wrapper<2>::bind(&my_class::function2);
    callbacks.callback3 = wrapper<3>::bind(&my_class::function3);

    callbacks.callback1a("Hello");
    callbacks.callback1b("World");
    callbacks.callback2(1.2f);
    callbacks.callback3("Hello", "World");

    return 0;
}


Dat geeft de volgende output:
code:
1
2
3
4
function1a: Hello
function1b: World
function2: 1.2
function3: Hello, World


Het vervelende is dat function1a en function1b dezelfde signatuur hebben; hierdoor zou een template dezelfde functie genereren en kun je runtime geen onderscheid meer maken tussen beide. Dit is de reden dat ik het "Id" template argument heb moeten toevoegen, hiermee verplicht ik de compiler voor iedere instantie nieuwe code te genereren. Dit kan fraaier als de C interface wel opaque pointers heeft. Let dus op dat alle Ids uniek moeten zijn, als je ze allemaal op 0 zet krijg je de volgende output: (de eerste call gaat nu naar function1b in plaats van function1a)
code:
1
2
3
4
function1b: Hello
function1b: World
function2: 1.2
function3: Hello, World

Too many people, making too many problems


Acties:
  • 0 Henk 'm!

  • cyberstalker
  • Registratie: September 2005
  • Niet online

cyberstalker

Eersteklas beunhaas

Topicstarter
Dit is een interessante aanpak waar ik zelf nog niet op was gekomen. Heeft dit niet als aanpak dat het moeilijker wordt voor de compiler om de functie te inlinen? Ik kan me voorstellen dat als de functie die moet worden aangeroepen een template parameter is dat heel makkelijk is (want dat staat al vast tijdens compilatie) terwijl het met deze code nog zou kunnen worden aangepast (en dus niet geinlined).

De library die ik wil gebruiken is event-driven (en ondersteunt geen multi-threading) en heeft dus een functie waarmee je een eigen void-pointer kunt opvragen. Het verhaal met die static instance is dus niet van toepassing (zoals reeds in de TS vermeld).

Ik ontken het bestaan van IE.


Acties:
  • 0 Henk 'm!

  • LeX-333
  • Registratie: Maart 2004
  • Laatst online: 21-11-2016
Een compiler zal calls via een functie pointer niet inline kunnen maken omdat het adres van de functie pas runtime beschikbaar is. De oplossing die ik voorstel gebruikt 2 functie pointers; de eerste is uiteraard de callback van de C library, de tweede is de pointer-to-member-functie naar de klasse die in de wrap struct zit. Dus beide zullen een heel klein beetje overhead geven, vergelijkbaar met een virtual functie call.

Too many people, making too many problems