Funzione passata come argomento modello


225

Sto cercando le regole che implicano il passaggio delle funzioni dei modelli C ++ come argomenti.

Questo è supportato da C ++ come mostrato da un esempio qui:

#include <iostream>

void add1(int &v)
{
  v+=1;
}

void add2(int &v)
{
  v+=2;
}

template <void (*T)(int &)>
void doOperation()
{
  int temp=0;
  T(temp);
  std::cout << "Result is " << temp << std::endl;
}

int main()
{
  doOperation<add1>();
  doOperation<add2>();
}

Conoscere questa tecnica è tuttavia difficile. Cercare su Google "funzione come argomento modello" non porta a molto. E anche i classici modelli C ++ La Guida Completa sorprendentemente non ne discutono (almeno non dalla mia ricerca).

Le domande che ho sono se questo è C ++ valido (o solo qualche estensione ampiamente supportata).

Inoltre, esiste un modo per consentire a un funzione con la stessa firma di essere utilizzata in modo intercambiabile con funzioni esplicite durante questo tipo di invocazione del modello?

Quanto segue non funziona nel programma precedente, almeno in Visual C ++ , poiché la sintassi è ovviamente errata. Sarebbe bello poter cambiare una funzione per un functor e viceversa, in modo simile al modo in cui è possibile passare un puntatore a una funzione o un funzione per l'algoritmo std :: sort se si desidera definire un'operazione di confronto personalizzata.

   struct add3 {
      void operator() (int &v) {v+=3;}
   };
...

    doOperation<add3>();

I puntatori a un collegamento web o due, o una pagina nel libro Modelli C ++ sarebbero apprezzati!


Qual è il vantaggio di una funzione come argomento template? Il tipo restituito non verrebbe utilizzato come tipo di modello?
DaClown,

Correlati: un lambda senza acquisizioni può decadere in un puntatore a funzione e puoi passarlo come parametro template in C ++ 17. Clang lo compila bene, ma l'attuale gcc (8.2) ha un bug e lo rifiuta in modo errato come "senza collegamento" anche con -std=gnu++17. Posso usare il risultato di un operatore di conversione lambda constexpr C ++ 17 captureless come argomento non tipo del modello di puntatore a funzione? .
Peter Cordes,

Risposte:


124

Sì, è valido

Per quanto riguarda il suo funzionamento anche con i funzioni, la solita soluzione è qualcosa di simile invece:

template <typename F>
void doOperation(F f)
{
  int temp=0;
  f(temp);
  std::cout << "Result is " << temp << std::endl;
}

che ora può essere chiamato come:

doOperation(add2);
doOperation(add3());

Guardalo dal vivo

Il problema con questo è che se rende complicato il compilatore per incorporare la chiamata add2, poiché tutto il compilatore sa è che void (*)(int &)viene passato un tipo di puntatore a funzione doOperation. (Ma add3, essendo un functor, può essere facilmente integrato. Qui, il compilatore sa che un oggetto di tipo add3viene passato alla funzione, il che significa che la funzione da chiamare è add3::operator(), e non solo un puntatore a funzione sconosciuta.)


19
Ora ecco una domanda interessante. Quando viene passato il nome di una funzione, NON è come se fosse coinvolto un puntatore a funzione. È una funzione esplicita, data in fase di compilazione. Quindi il compilatore sa esattamente cosa ha al momento della compilazione.
SPWorley,

1
Vi è un vantaggio nell'usare i funzione rispetto ai puntatori a funzione. Il functor può essere istanziato all'interno della classe e quindi fornisce più funzionalità al compilatore per le ottimizzazioni (come inline). Il compilatore sarebbe difficile da premere per ottimizzare una chiamata tramite un puntatore a funzione.
Martin York,

11
Quando la funzione viene utilizzata in un parametro modello, "decade" in un puntatore alla funzione passata. È analogo al modo in cui gli array decadono in puntatori quando vengono passati come argomenti ai parametri. Naturalmente, il valore del puntatore è noto al momento della compilazione e deve puntare a una funzione con collegamento esterno in modo che il compilatore possa utilizzare queste informazioni a fini di ottimizzazione.
CB Bailey,

5
Avanti rapidamente a qualche anno dopo, la situazione con l'uso delle funzioni come argomenti modello è migliorata molto in C ++ 11. Non sei più obbligato a utilizzare i Javaism come le classi functor e puoi utilizzare direttamente le funzioni inline statiche come argomenti template. Parla ancora molto lontano rispetto alle macro Lisp degli anni '70, ma C ++ 11 ha sicuramente fatto buoni progressi nel corso degli anni.
falco

5
dal momento che c ++ 11 non sarebbe meglio prendere la funzione come rvalue reference ( template <typename F> void doOperation(F&& f) {/**/}), quindi bind per esempio può passare un'espressione bind invece di legarla?
user1810087

70

I parametri del modello possono essere parametrizzati per tipo (nome tipografico T) o per valore (int X).

Il modo "tradizionale" C ++ di creare un modello di codice consiste nell'utilizzare un functor, ovvero il codice si trova in un oggetto e l'oggetto conferisce quindi al codice un tipo unico.

Quando si lavora con funzioni tradizionali, questa tecnica non funziona bene, poiché un cambio di tipo non indica una funzione specifica , ma specifica solo la firma di molte possibili funzioni. Così:

template<typename OP>
int do_op(int a, int b, OP op)
{
  return op(a,b);
}
int add(int a, int b) { return a + b; }
...

int c = do_op(4,5,add);

Non equivale al caso di funzione. In questo esempio, do_op è istanziato per tutti i puntatori a funzioni la cui firma è int X (int, int). Il compilatore dovrebbe essere piuttosto aggressivo per includere pienamente questo caso. (Non lo escluderei però, poiché l'ottimizzazione del compilatore è diventata piuttosto avanzata.)

Un modo per dire che questo codice non fa esattamente quello che vogliamo è:

int (* func_ptr)(int, int) = add;
int c = do_op(4,5,func_ptr);

è ancora legale, e chiaramente questo non viene sottolineato. Per ottenere il massimo in linea, è necessario creare un modello in base al valore, quindi la funzione è completamente disponibile nel modello.

typedef int(*binary_int_op)(int, int); // signature for all valid template params
template<binary_int_op op>
int do_op(int a, int b)
{
 return op(a,b);
}
int add(int a, int b) { return a + b; }
...
int c = do_op<add>(4,5);

In questo caso, ogni versione istanziata di do_op è istanziata con una funzione specifica già disponibile. Quindi ci aspettiamo che il codice di do_op assomigli molto a "return a + b". (Programmatori Lisp, smetti di sorridere!)

Possiamo anche confermare che questo è più vicino a ciò che vogliamo perché questo:

int (* func_ptr)(int,int) = add;
int c = do_op<func_ptr>(4,5);

non verrà compilato. GCC dice: "errore: 'func_ptr' non può apparire in un'espressione costante. In altre parole, non posso espandere completamente do_op perché non mi hai dato abbastanza informazioni al momento del compilatore per sapere qual è la nostra operazione.

Quindi se il secondo esempio sta davvero integrando completamente la nostra operazione, e il primo no, a che serve il modello? Cosa sta facendo? La risposta è: digitare la coercizione. Questo riff sul primo esempio funzionerà:

template<typename OP>
int do_op(int a, int b, OP op) { return op(a,b); }
float fadd(float a, float b) { return a+b; }
...
int c = do_op(4,5,fadd);

Quell'esempio funzionerà! (Non sto suggerendo che sia un buon C ++ ma ...) Quello che è successo è do_op è stato modellato attorno alle firme delle varie funzioni, e ogni istanza separata scriverà un codice di coercizione di tipo diverso. Quindi il codice istanziato per do_op con fadd è simile al seguente:

convert a and b from int to float.
call the function ptr op with float a and float b.
convert the result back to int and return it.

In confronto, il nostro caso per valore richiede una corrispondenza esatta sugli argomenti della funzione.


2
Vedere stackoverflow.com/questions/13674935/… per una domanda di follow-up in risposta diretta all'osservazione qui che int c = do_op(4,5,func_ptr);"chiaramente non viene allineata".
Dan Nissenbaum,

Vedi qui per un esempio di questo essere inline: stackoverflow.com/questions/4860762/... Sembra che i compilatori sono sempre piuttosto intelligente in questi giorni.
BigSandwich,

15

I puntatori a funzione possono essere passati come parametri del modello e questo fa parte del C ++ standard . Tuttavia, nel modello vengono dichiarate e utilizzate come funzioni anziché come puntatore a funzione. All'istanza del modello si passa l'indirizzo della funzione anziché solo il nome.

Per esempio:

int i;


void add1(int& i) { i += 1; }

template<void op(int&)>
void do_op_fn_ptr_tpl(int& i) { op(i); }

i = 0;
do_op_fn_ptr_tpl<&add1>(i);

Se si desidera passare un tipo di funzione come argomento modello:

struct add2_t {
  void operator()(int& i) { i += 2; }
};

template<typename op>
void do_op_fntr_tpl(int& i) {
  op o;
  o(i);
}

i = 0;
do_op_fntr_tpl<add2_t>(i);

Diverse risposte passano un'istanza di funzione come argomento:

template<typename op>
void do_op_fntr_arg(int& i, op o) { o(i); }

i = 0;
add2_t add2;

// This has the advantage of looking identical whether 
// you pass a functor or a free function:
do_op_fntr_arg(i, add1);
do_op_fntr_arg(i, add2);

Il più vicino a questo aspetto uniforme con un argomento modello è definire do_opdue volte una volta con un parametro non di tipo e una volta con un parametro di tipo.

// non-type (function pointer) template parameter
template<void op(int&)>
void do_op(int& i) { op(i); }

// type (functor class) template parameter
template<typename op>
void do_op(int& i) {
  op o; 
  o(i); 
}

i = 0;
do_op<&add1>(i); // still need address-of operator in the function pointer case.
do_op<add2_t>(i);

Onestamente, mi aspettavo davvero che non si compilasse, ma ha funzionato per me con gcc-4.8 e Visual Studio 2013.


9

Nel tuo modello

template <void (*T)(int &)>
void doOperation()

Il parametro Tè un parametro modello non di tipo. Ciò significa che il comportamento della funzione modello cambia con il valore del parametro (che deve essere risolto in fase di compilazione, quali sono le costanti del puntatore a funzione).

Se vuoi qualcosa che funzioni sia con gli oggetti funzione che con i parametri funzione, hai bisogno di un modello digitato. Quando si esegue questa operazione, tuttavia, è necessario fornire anche un'istanza di oggetto (istanza di oggetto funzione o un puntatore di funzione) alla funzione in fase di esecuzione.

template <class T>
void doOperation(T t)
{
  int temp=0;
  t(temp);
  std::cout << "Result is " << temp << std::endl;
}

Vi sono alcune considerazioni minori sulle prestazioni. Questa nuova versione potrebbe essere meno efficiente con gli argomenti del puntatore a funzione in quanto il particolare puntatore a funzione è solo definito e richiamato in fase di esecuzione mentre il modello di puntatore a funzione può essere ottimizzato (possibilmente la chiamata di funzione in linea) in base al particolare puntatore a funzione utilizzato. Gli oggetti funzione possono spesso essere espansi in modo molto efficiente con il modello tipizzato, sebbene il particolare operator()sia completamente determinato dal tipo di oggetto funzione.


1

Il motivo per cui il tuo esempio di funzione non funziona è che è necessaria un'istanza per invocare il file operator().


0

Modifica: il passaggio dell'operatore come riferimento non funziona. Per semplicità, capiscilo come un puntatore a funzione. Basta inviare il puntatore, non un riferimento. Penso che tu stia provando a scrivere qualcosa del genere.

struct Square
{
    double operator()(double number) { return number * number; }
};

template <class Function>
double integrate(Function f, double a, double b, unsigned int intervals)
{
    double delta = (b - a) / intervals, sum = 0.0;

    while(a < b)
    {
        sum += f(a) * delta;
        a += delta;
    }

    return sum;
}

. .

std::cout << "interval : " << i << tab << tab << "intgeration = "
 << integrate(Square(), 0.0, 1.0, 10) << std::endl;
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.