Função passada como argumento do modelo

224

Estou procurando as regras que envolvem a passagem de modelos C ++ como argumentos.

Isso é suportado pelo C ++, como mostra um exemplo aqui:

#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>();
}

Aprender sobre essa técnica é difícil, no entanto. Pesquisar no Google "funcionar como argumento de modelo" não leva a muito. E, surpreendentemente, os clássicos modelos C ++ O Guia Completo também não o discute (pelo menos não na minha pesquisa).

As perguntas que tenho são se este é C ++ válido (ou apenas alguma extensão amplamente suportada).

Além disso, existe uma maneira de permitir que um functor com a mesma assinatura seja usado de forma intercambiável com funções explícitas durante esse tipo de chamada de modelo?

O seguinte não funciona no programa acima, pelo menos no Visual C ++ , porque a sintaxe está obviamente errada. Seria bom poder alternar uma função para um functor e vice-versa, semelhante à maneira como você pode passar um ponteiro de função ou functor para o algoritmo std :: sort, se desejar definir uma operação de comparação personalizada.

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

    doOperation<add3>();

Ponteiros para um link da Web ou dois, ou uma página no livro Modelos C ++ seriam apreciados!

SPWorley
fonte
Qual é o benefício de uma função como argumento de modelo? O tipo de retorno não seria usado como o tipo de modelo?
DaClown
Relacionado: um lambda sem capturas pode se deteriorar para um ponteiro de função e você pode passar isso como um parâmetro de modelo no C ++ 17. O Clang o compila ok, mas o gcc atual (8.2) tem um bug e o rejeita incorretamente por não ter "nenhuma ligação", mesmo com ele -std=gnu++17. Posso usar o resultado de um operador de conversão constexpr lambda capturável do C ++ 17 como argumento não-tipo de modelo de ponteiro de função? .
6268 Peter Cordes

Respostas:

123

Sim, é válido.

Quanto a fazê-lo funcionar com functores, a solução usual é algo como isto:

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

que agora pode ser chamado como:

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

Veja ao vivo

O problema é que, se for complicado para o compilador incorporar a chamada add2, uma vez que tudo o que o compilador sabe é que void (*)(int &)está sendo passado um tipo de ponteiro de função doOperation. (Mas add3, sendo um functor, pode ser embutido facilmente. Aqui, o compilador sabe que um objeto do tipo add3é passado para a função, o que significa que a função a ser chamada é add3::operator(), e não apenas um ponteiro de função desconhecido).

jalf
fonte
19
Agora, aqui está uma pergunta interessante. Quando um nome de função é passado, NÃO é como se houvesse um ponteiro de função envolvido. É uma função explícita, fornecida em tempo de compilação. Portanto, o compilador sabe exatamente o que é obtido no momento da compilação.
23409 SPWorley
1
Há uma vantagem em usar functors sobre ponteiros de função. O functor pode ser instanciado dentro da classe e, portanto, fornece mais oportunidades ao compilador para otimizações (como inlining). O compilador seria pressionado para otimizar uma chamada sobre um ponteiro de função.
Martin Iorque
11
Quando a função é usada em um parâmetro de modelo, ela 'decai' em um ponteiro para a função passada. É análogo a como as matrizes se decompõem em ponteiros quando passadas como argumentos para parâmetros. Obviamente, o valor do ponteiro é conhecido no momento da compilação e deve apontar para uma função com ligação externa, para que o compilador possa usar essas informações para fins de otimização.
22310 CB Bailey
5
Avançando rapidamente para alguns anos depois, a situação com o uso de funções como argumentos de modelo melhorou muito no C ++ 11. Você não precisa mais usar Javaisms como classes de functor e pode usar, por exemplo, funções embutidas estáticas como argumentos de modelo diretamente. Ainda está longe de ser comparado às macros Lisp da década de 1970, mas o C ++ 11 definitivamente tem um bom progresso ao longo dos anos.
Pfalcon
5
como c ++ 11 não seria melhor tomar a função como rvalue reference ( template <typename F> void doOperation(F&& f) {/**/}), então bind, por exemplo, pode passar uma expressão de bind em vez de vinculá-la?
user1810087
70

Os parâmetros do modelo podem ser parametrizados por tipo (nome do tipo T) ou por valor (int X).

A maneira "tradicional" de C ++ de modelar um pedaço de código é usar um functor - ou seja, o código está em um objeto e, assim, o objeto fornece ao tipo único de código.

Ao trabalhar com funções tradicionais, essa técnica não funciona bem, porque uma alteração no tipo não indica uma função específica - mas especifica apenas a assinatura de muitas funções possíveis. Assim:

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);

Não é equivalente ao caso do functor. Neste exemplo, do_op é instanciado para todos os ponteiros de função cuja assinatura é int X (int, int). O compilador teria que ser bastante agressivo para integrar totalmente esse caso. (Eu não descartaria isso, pois a otimização do compilador ficou bastante avançada.)

Uma maneira de dizer que esse código não faz exatamente o que queremos é:

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

ainda é legal e, claramente, isso não está sendo incorporado. Para obter informações completas, precisamos modelar por valor, para que a função esteja totalmente disponível no modelo.

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);

Nesse caso, cada versão instanciada do do_op é instanciada com uma função específica já disponível. Portanto, esperamos que o código para do_op seja muito parecido com "return a + b". (Programadores Lisp, parem de sorrir!)

Também podemos confirmar que isso está mais próximo do que queremos, porque isso:

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

falhará na compilação. O GCC diz: "error: 'func_ptr' não pode aparecer em uma expressão constante. Em outras palavras, não consigo expandir completamente do_op porque você não me forneceu informações suficientes no tempo do compilador para saber qual é a nossa operação.

Então, se o segundo exemplo está realmente alinhando totalmente a nossa operação, e o primeiro não, qual é o bom modelo? O que isso está fazendo? A resposta é: digite coerção. Este riff no primeiro exemplo funcionará:

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);

Esse exemplo vai funcionar! (Não estou sugerindo que seja bom C ++, mas ...) O que aconteceu é que do_op foi modelado em torno das assinaturas das várias funções, e cada instanciação separada escreverá um código de coerção de tipo diferente. Portanto, o código instanciado para do_op com fadd se parece com:

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.

Por comparação, nosso caso por valor requer uma correspondência exata nos argumentos da função.

Ben Supnik
fonte
2
Consulte stackoverflow.com/questions/13674935/… para uma pergunta de acompanhamento em resposta direta à observação aqui que int c = do_op(4,5,func_ptr);"claramente não está sendo incorporada".
Dan Nissenbaum 3/12/12
Veja aqui um exemplo disso: stackoverflow.com/questions/4860762/… Parece que os compiladores estão ficando bastante inteligentes nos dias de hoje.
BigSandwich
15

Os ponteiros de função podem ser passados ​​como parâmetros de modelo, e isso faz parte do C ++ padrão . No entanto, no modelo, eles são declarados e usados ​​como funções, em vez de ponteiro para função. Na instanciação do modelo, passa-se o endereço da função em vez de apenas o nome.

Por exemplo:

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 você deseja passar um tipo de functor como um argumento de modelo:

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);

Várias respostas passam uma instância de functor como argumento:

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);

O mais próximo que você pode chegar dessa aparência uniforme com um argumento de modelo é definir do_opduas vezes - uma vez com um parâmetro não-tipo e uma vez com um parâmetro-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);

Honestamente, eu realmente esperava que isso não fosse compilado, mas funcionou para mim com o gcc-4.8 e o Visual Studio 2013.

Kietz
fonte
9

No seu modelo

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

O parâmetro T é um parâmetro de modelo não-tipo. Isso significa que o comportamento da função do modelo muda com o valor do parâmetro (que deve ser corrigido no tempo de compilação, quais são as constantes do ponteiro da função).

Se você deseja algo que funcione com objetos de função e parâmetros de função, precisa de um modelo digitado. Ao fazer isso, no entanto, você também precisa fornecer uma instância de objeto (instância de objeto de função ou um ponteiro de função) para a função em tempo de execução.

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

Existem algumas considerações menores sobre desempenho. Essa nova versão pode ser menos eficiente com argumentos de ponteiro de função, pois o ponteiro de função específico é apenas desprotegido e chamado em tempo de execução, enquanto o modelo de ponteiro de função pode ser otimizado (possivelmente a chamada de função incorporada) com base no ponteiro de função específico usado. Os objetos de função geralmente podem ser expandidos de maneira muito eficiente com o modelo digitado, embora o particular operator()seja completamente determinado pelo tipo do objeto de função.

CB Bailey
fonte
1

A razão pela qual seu exemplo de functor não funciona é que você precisa de uma instância para chamar o operator().

Nikolai Fetissov
fonte
0

Editar: Passar o operador como referência não funciona. Para simplificar, entenda-o como um ponteiro de função. Você acabou de enviar o ponteiro, não uma referência. Eu acho que você está tentando escrever algo assim.

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;
AraK
fonte