Check alle échte Black Friday-deals Ook zo moe van nepaanbiedingen? Wij laten alleen échte deals zien

[C++] Variadic template function overloading

Pagina: 1
Acties:

  • cyberstalker
  • Registratie: September 2005
  • Niet online

cyberstalker

Eersteklas beunhaas

Topicstarter
Ik zit al een tijdje te stoeien met variadic templates en function overloading. Ik wil twee functies die, buiten een optionele callback parameter, dezelfde signatuur hebben. Als voorbeeld nemen we de volgende twee functies:

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
/**
  * Execute something, without letting us know the result.
  *
  * @param  callback    callback to run when execution completes
  * @param  mixed...    parameters
  */
template <typename ...Arguments>
void execute(const std::function<void()>& callback, const Arguments& ...parameters)
{
    // construct parameters
    Parameter params[sizeof...(parameters)]{ parameters... };

    // execute here in another thread
}

/**
  * Execute something, without letting us know the result.
  *
  * @param  mixed...    parameters
  */
template <typename ...Arguments>
void execute(const Arguments& ...parameters)
{
    // construct parameters
    Parameter params[sizeof...(parameters)]{ parameters... };

    // execute here in another thread
}


Waarbij de Parameter klasse zo is gemaakt dat deze een lijst van opties ondersteunt voor de constructor, bijvoorbeeld zo:

C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
  * Class representing a single parameter
  */
class Parameter
{
public:
    /**
      * Constructor
      *
      * @param  value   parameter value
      */
    Parameter(const char *value) {}

    /**
      * Constructor
      *
      * @param  value   parmeter value
      */
    Parameter(uint64_t value) {}
};


Nu zijn er dus logischerwijs twee manieren om de functie execute() aan te roepen:

C++:
1
2
3
4
5
6
// this fails, because it tries to link to the second
// callback type, should do something with std::enable_if
execute([]() {}, "parameter 1", 2, "parameter 3");

// this works of course, all parameters can be converted to a Parameter
execute("parameter 1", 2, "parameter 3");


Sowieso vind ik het al vreemd dat dit niet compileert: de compiler blijft eigenwijs proberen de versie van execute() te gebruiken zonder callback, merkt dat dit niet kan maar is niet in staat de andere versie te gebruiken. De enige manier waarop dat wel werkt is om zelf de lambda al te wrappen in een std::function object (wat een veel langere, onduidelijkere syntax geeft).

De enige oplossing hiervoor is om te forceren dat alle parameters uit het parameter pack geldige constructor arguments zijn voor Parameter. Hier wil ik dus std::enable_if voor gebruiken. Mijn eerste poging was als volgt:

C++:
1
template <typename ...Arguments, class = typename std::enable_if<std::is_constructible<Parameter, Arguments>::value...>::type>


Deze werkt echter enkel wanneer je deze aanroept met een enkel argument. enable_if houdt blijkbaar niet zo van arrays. Dus maar een simpele functie gemaakt die je met een parameter pack kunt aanroepen en die een enkele boolean retourneert. Netjes constexpr, dus dat zou moeten werken:

C++:
1
2
3
4
5
6
7
8
9
10
/**
  * Constant expression to check whether all parameters
  * are valid constructor arguments for the requested
  * class.
  *
  * This does not work, as it is ambiguous, the second
  * version is also valid with an empty parameter pack.
  */
template <typename T> constexpr bool validParameter() { return std::is_constructible<Parameter, T>::value; }
template <typename T, typename ...More> constexpr bool validParameter() { return std::is_constructible<Parameter, T>::value && validParameter<More...>(); }


Dit werkt niet omdat de compiler niet begrijpt welke variant je wilt aanroepen. De tweede versie is ook geldig met een lege lijst (dit werkt enkel met variabelen, niet met types blijkbaar).

Hier zit ik dus een beetje vast. Er moeten twee variaties zijn omdat anders de compiler blijft uitrollen terwijl alle parameters op zijn (wat dus een error geeft dat er geen T meer is), maar er kunnen geen twee variaties zijn omdat de compiler dat niet slikt.

Een enkele variant zou natuurlijk leuker zijn, als je bijvoorbeeld iets als dit zou kunnen doen:
C++:
1
2
3
4
5
6
7
8
9
/**
  * Constant expression to check whether all parameters
  * are valid constructor arguments for the requested
  * class.
  *
  * This does not work, as it is ambiguous, the second
  * version is also valid with an empty parameter pack.
  */
template <typename T, typename ...More> constexpr bool validParameter() { return std::is_constructible<Parameter, T>::value && sizeof...(More) > 0 ? validParameter<More...>() : true; }


wat natuurlijk ook niet werkt omdat, ondanks dat de ternary operator er voor zorgt dat de functie niet wordt aangeroepen als er geen argumenten meer zijn, deze wel moet bestaan (en dat is natuurlijk niet zo).

Ik heb alles geprobeerd dat ik kan bedenken. Heeft iemand suggesties hoe ik dit aan de praat kan krijgen?

Ik ontken het bestaan van IE.


  • Bob
  • Registratie: Mei 2005
  • Laatst online: 20-11 21:06

Bob

Je kan de signature wat generieker maken, waardoor hij sowieso ook lambda's accepteert:

C++:
1
2
3
4
5
6
7
8
template <typename Func, typename ...Arguments> 
void execute(const Func& callback, const Arguments& ...parameters) 
{ 
    // construct parameters 
    Parameter params[sizeof...(parameters)]{ parameters... }; 

    // execute here in another thread 
} 

  • cyberstalker
  • Registratie: September 2005
  • Niet online

cyberstalker

Eersteklas beunhaas

Topicstarter
Een slimme suggestie, hiermee krijg ik de code inderdaad gecompileerd. De functie accepteert nu natuurlijk wel alles (ook als het geen geldige callback is), wat natuurlijk niet compileert zodra je deze wilt uitvoeren.

Dit leek me simpel op te lossen door te testen of Func een geldige constructorwaarde is voor een std::function (dan is het natuurlijk gewoon aan te roepen). Deze enable_if houdt echter helemaal niets tegen. Of ik nu de enable_if aan het einde zet of voor de Arguments maakt niet uit.

Het lijkt er dus op enable_if niet werkt in combinatie met meerdere template parameters. Waarschijnlijk omdat dat zelf ook een template parameter is (met een default waarde). De parameter pack overschrijft die default waarde waardoor de functie dus altijd "geldig" is (en dan vervolgens weer niet compileert).
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
  * Execute something, letting us know the result.
  *
  * @param  callback    callback to run when execution completes
  * @param  mixed...    parameters
  */
template <typename Func, class = typename std::enable_if<std::is_constructible<std::function<void()>, Func>::value>::type, typename ...Arguments>
void execute(const Func& callback, const Arguments& ...parameters)
{
    // construct parameters
    Parameter params[sizeof...(parameters)]{ parameters... };

    // execute here in another thread
}

Ik ontken het bestaan van IE.


  • Bob
  • Registratie: Mei 2005
  • Laatst online: 20-11 21:06

Bob

Je houd van ingewikkelde oplossingen lijkt me :) Persoonlijk zou ik het bij de generieke vorm laten, en er vanuit gaan dat de compiler het opvangt wanneer ik een foute functie/whatever meegeef. Als je toch de signature wat strakker wil checken doe je het eventueel zo:

C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/** 
  * Execute something, letting us know the result. 
  * 
  * @param  callback    callback to run when execution completes 
  * @param  mixed...    parameters 
  */ 
template <typename Func, typename ...Arguments> 
void execute(const Func& callback, const Arguments& ...parameters) 
{ 
    // assign callback to a std::function, compiler will freak out when this is not possible
    std::function<void()> func = callback;
    // construct parameters 
    Parameter params[sizeof...(parameters)]{ parameters... }; 

    // execute here in another thread 

    // use callback, aka func
}

  • cyberstalker
  • Registratie: September 2005
  • Niet online

cyberstalker

Eersteklas beunhaas

Topicstarter
Geloof me: ik hou helemaal niet van ingewikkelde oplossingen. Als wat jij voorstelt zou werken zou ik dat meteen doen. Het probleem is dat de compiler dan wel doorheeft dat het van bijvoorbeeld een uint64_t geen std::function kan maken, maar niet zo slim is dan maar die andere variant (die immers ook geldig is en het wel doet) te pakken.

Het is echt uit pure noodzaak dat ik het wil afvangen.

[ Voor 55% gewijzigd door cyberstalker op 11-03-2014 16:26 ]

Ik ontken het bestaan van IE.


  • MSalters
  • Registratie: Juni 2001
  • Laatst online: 13-09 00:05
Je begrijpt blijkbaar niet hoe overloading werkt. Dat is een mechanisme wat werkt op basis van de signature van een functie, niet van de definitie. De compiler blijft niet "eigenwijs proberen"; de overload resolutie kiest de beste overload (exact match) omdat het alternatief een conversie heeft (lamda naar std::function).

Het fundamentele probleem is jouw signature keuze. Hoe geef je aan de eerste versie een Argument van lambda-type mee?

Anyway, de correcte oplossing is inderdaad een std::enable_if, maar dan simpelweg als return type:
C++:
1
2
3
template <typename Func, typename ...Arguments> 
std::enable_if<std::is_constructible<std::function<void()>, Func>::value, void>::type
execute(const Func& callback, const Arguments& ...parameters);

Man hopes. Genius creates. Ralph Waldo Emerson
Never worry about theory as long as the machinery does what it's supposed to do. R. A. Heinlein


  • cyberstalker
  • Registratie: September 2005
  • Niet online

cyberstalker

Eersteklas beunhaas

Topicstarter
Daar heb je een beetje gelijk: Ik had me inderdaad niet gerealiseerd dat de compiler in mijn eerste opzet de versie met enkel het parameter pack kiest omdat daar geen conversie nodig is.

Het idee van Bob heeft dit echter niet, en ook daar kiest de compiler een versie en blijft daarop haken (terwijl voor beide versies geen conversie nodig is). Hier zou je eigenlijk een melding over een ambiguous functie verwachten. Maar dat terzijde.

Met een kleine aanpassing (nog een typename voor de std::enable_if zetten) werkt het nu gelukkig wel, met uitzondering van const char* en std::string, want volgens is_constructible mag een std::function daar gewoon mee worden construct (maar als je dat dan probeert compileert het natuurlijk niet).

Ik zal hiervoor dus zelf een trait moeten maken (is_callable bijvoorbeeld) om te zien of het gegeven object een operator() heeft. Ik weet nu in ieder geval waar ik deze moet gaan neerzetten zodat het werkt.

Bedankt voor alle hulp!

Ik ontken het bestaan van IE.


  • MSalters
  • Registratie: Juni 2001
  • Laatst online: 13-09 00:05
Dat laatste klinkt als een bug in je `std::is_constructible` implementatie.

Wat betreft het originele overloading probleem, dat zijn primair geordend via de benodigde conversies. Dat wil niet zeggen dat er geen secundaire ordening is. Gewone functies gaan boven templates, etcetera. In dit geval gaat een "Func" template argument boven een extra parameter in het parameter pack - vandaar de std::enable_if om die betere overload te negeren als de Func parameter niet function-like is.

Man hopes. Genius creates. Ralph Waldo Emerson
Never worry about theory as long as the machinery does what it's supposed to do. R. A. Heinlein

Pagina: 1