c++ - functors - std::function




Quais são os functores de C++ e seus usos? (10)

Aqui está uma situação real em que fui forçado a usar um Functor para resolver o meu problema:

Eu tenho um conjunto de funções (digamos, 20 delas), e elas são todas idênticas, exceto que cada uma chama uma função específica diferente em 3 pontos específicos.

Isso é incrível desperdício e duplicação de código. Normalmente eu apenas passaria um ponteiro de função, e apenas chamaria isso nos 3 pontos. (Assim, o código só precisa aparecer uma vez, em vez de vinte vezes.)

Mas então percebi, em cada caso, que a função específica exigia um perfil de parâmetro completamente diferente! Às vezes 2 parâmetros, às vezes 5 parâmetros, etc.

Outra solução seria ter uma classe base, em que a função específica é um método substituído em uma classe derivada. Mas eu realmente quero construir toda essa HERANÇA, só para poder passar um ponteiro de função ????

SOLUÇÃO: Então o que eu fiz foi, eu fiz uma classe wrapper (um "Functor") que é capaz de chamar qualquer uma das funções que eu precisava chamar. Eu configuro-o antecipadamente (com seus parâmetros, etc) e depois passo em vez de um ponteiro de função. Agora o código chamado pode disparar o Functor, sem saber o que está acontecendo no interior. Pode até chamá-lo várias vezes (eu precisava chamar 3 vezes).

É isso - um exemplo prático em que um Functor acabou sendo a solução óbvia e fácil, o que me permitiu reduzir a duplicação de código de 20 funções para 1.

Eu continuo ouvindo muito sobre functores em C ++. Alguém pode me dar uma visão geral sobre o que eles são e em quais casos eles seriam úteis?


Como foi repetido, os functores são classes que podem ser tratadas como funções (operador de sobrecarga ()).

Eles são mais úteis para situações em que você precisa associar alguns dados a chamadas repetidas ou atrasadas para uma função.

Por exemplo, uma lista encadeada de functores poderia ser usada para implementar um sistema básico de co-rotina síncrona de baixa sobrecarga, um despachador de tarefas ou uma análise de arquivo interrompível. Exemplos:

/* prints "this is a very simple and poorly used task queue" */
class Functor
{
public:
    std::string output;
    Functor(const std::string& out): output(out){}
    operator()() const
    {
        std::cout << output << " ";
    }
};

int main(int argc, char **argv)
{
    std::list<Functor> taskQueue;
    taskQueue.push_back(Functor("this"));
    taskQueue.push_back(Functor("is a"));
    taskQueue.push_back(Functor("very simple"));
    taskQueue.push_back(Functor("and poorly used"));
    taskQueue.push_back(Functor("task queue"));
    for(std::list<Functor>::iterator it = taskQueue.begin();
        it != taskQueue.end(); ++it)
    {
        *it();
    }
    return 0;
}

/* prints the value stored in "i", then asks you if you want to increment it */
int i;
bool should_increment;
int doSomeWork()
{
    std::cout << "i = " << i << std::endl;
    std::cout << "increment? (enter the number 1 to increment, 0 otherwise" << std::endl;
    std::cin >> should_increment;
    return 2;
}
void doSensitiveWork()
{
     ++i;
     should_increment = false;
}
class BaseCoroutine
{
public:
    BaseCoroutine(int stat): status(stat), waiting(false){}
    void operator()(){ status = perform(); }
    int getStatus() const { return status; }
protected:
    int status;
    bool waiting;
    virtual int perform() = 0;
    bool await_status(BaseCoroutine& other, int stat, int change)
    {
        if(!waiting)
        {
            waiting = true;
        }
        if(other.getStatus() == stat)
        {
            status = change;
            waiting = false;
        }
        return !waiting;
    }
}

class MyCoroutine1: public BaseCoroutine
{
public:
    MyCoroutine1(BaseCoroutine& other): BaseCoroutine(1), partner(other){}
protected:
    BaseCoroutine& partner;
    virtual int perform()
    {
        if(getStatus() == 1)
            return doSomeWork();
        if(getStatus() == 2)
        {
            if(await_status(partner, 1))
                return 1;
            else if(i == 100)
                return 0;
            else
                return 2;
        }
    }
};

class MyCoroutine2: public BaseCoroutine
{
public:
    MyCoroutine2(bool& work_signal): BaseCoroutine(1), ready(work_signal) {}
protected:
    bool& work_signal;
    virtual int perform()
    {
        if(i == 100)
            return 0;
        if(work_signal)
        {
            doSensitiveWork();
            return 2;
        }
        return 1;
    }
};

int main()
{
     std::list<BaseCoroutine* > coroutineList;
     MyCoroutine2 *incrementer = new MyCoroutine2(should_increment);
     MyCoroutine1 *printer = new MyCoroutine1(incrementer);

     while(coroutineList.size())
     {
         for(std::list<BaseCoroutine *>::iterator it = coroutineList.begin();
             it != coroutineList.end(); ++it)
         {
             *it();
             if(*it.getStatus() == 0)
             {
                 coroutineList.erase(it);
             }
         }
     }
     delete printer;
     delete incrementer;
     return 0;
}

Claro, esses exemplos não são úteis em si mesmos. Eles só mostram como os functores podem ser úteis, os functores em si são muito básicos e inflexíveis e isso os torna menos úteis do que, por exemplo, o que o boost fornece.


Eu tenho "descoberto" um uso muito interessante dos functores: eu os uso quando não tenho um bom nome para um método, como um functor é um método sem nome ;-)


Exceto para uso em retorno de chamada, os functores C ++ também podem ajudar a fornecer um estilo de acesso ao Matlab para uma classe de matriz . Existe um example .


O nome "functor" tem sido tradicionalmente usado na teoria de categorias muito antes de o C ++ aparecer em cena. Isso não tem nada a ver com o conceito C ++ de functor. É melhor usar o objeto de função de nome em vez do que chamamos de "functor" em C ++. É assim que outras linguagens de programação chamam construções semelhantes.

Usado em vez de função simples:

Características:

  • Objeto de função pode ter estado
  • O objeto de função se ajusta ao OOP (se comporta como qualquer outro objeto).

Contras:

  • Traz mais complexidade ao programa.

Usado em vez do ponteiro de função:

Características:

  • Objeto de função geralmente pode ser embutido

Contras:

  • Objeto de função não pode ser trocado com outro tipo de objeto de função durante o tempo de execução (pelo menos a menos que estenda alguma classe base, o que, portanto, gera alguma sobrecarga)

Usado em vez da função virtual:

Características:

  • Objeto de função (não virtual) não requer dispatching vtable e runtime, portanto, é mais eficiente na maioria dos casos

Contras:

  • Objeto de função não pode ser trocado com outro tipo de objeto de função durante o tempo de execução (pelo menos a menos que estenda alguma classe base, o que, portanto, gera alguma sobrecarga)

Os functores são usados ​​no gtkmm para conectar algum botão GUI a uma função ou método C ++ real.

Se você usar a biblioteca pthread para tornar seu aplicativo multithreaded, Functors pode ajudá-lo.
Para iniciar um thread, um dos argumentos do pthread_create(..) é o ponteiro de função a ser executado em seu próprio thread.
Mas há um inconveniente. Esse ponteiro não pode ser um ponteiro para um método, a menos que seja um método estático ou, a menos que você especifique sua classe , como class::method . E outra coisa, a interface do seu método só pode ser:

void* method(void* something)

Então você não pode executar (de uma forma simples e óbvia) métodos de sua classe em um thread sem fazer algo extra.

Uma maneira muito boa de lidar com encadeamentos em C ++ é criar sua própria classe Thread . Se você quisesse executar métodos da classe MyClass , o que fiz foi transformar esses métodos em classes derivadas do Functor .

Além disso, a classe Thread possui este método: static void* startThread(void* arg)
Um ponteiro para este método será usado como um argumento para chamar pthread_create(..) . E o que startThread(..) deve receber em arg é uma referência void* convertida para uma instância em heap de qualquer classe derivada de Functor , que será lançada de volta ao Functor* quando executada, e então chamada de método run() .


Para os novatos como eu entre nós: depois de uma pequena pesquisa, descobri o que o código jalf postou.

Um functor é um objeto de classe ou struct que pode ser "chamado" como uma função. Isso é possível sobrecarregando o () operator . O () operator (não sabe o que é chamado) pode receber qualquer número de argumentos. Outros operadores levam apenas dois, ou seja, o + operator só pode aceitar dois valores (um em cada lado do operador) e retornar o valor para o qual você o sobrecarregou. Você pode ajustar qualquer número de argumentos dentro de um () operator que é o que lhe dá flexibilidade.

Para criar um functor primeiro você cria sua classe. Então você cria um construtor para a classe com um parâmetro de sua escolha de tipo e nome. Isso é seguido na mesma instrução por uma lista de inicializadores (que usa um único operador de cólon, algo que eu também era novo) que constrói os objetos de membro de classe com o parâmetro declarado anteriormente para o construtor. Então o () operator está sobrecarregado. Finalmente você declara os objetos privados da classe ou estrutura que você criou.

Meu código (achei os nomes das variáveis ​​do jalf confusos)

class myFunctor
{ 
    public:
        /* myFunctor is the constructor. parameterVar is the parameter passed to
           the constructor. : is the initializer list operator. myObject is the
           private member object of the myFunctor class. parameterVar is passed
           to the () operator which takes it and adds it to myObject in the
           overloaded () operator function. */
        myFunctor (int parameterVar) : myObject( parameterVar ) {}

        /* the "operator" word is a keyword which indicates this function is an 
           overloaded operator function. The () following this just tells the
           compiler that () is the operator being overloaded. Following that is
           the parameter for the overloaded operator. This parameter is actually
           the argument "parameterVar" passed by the constructor we just wrote.
           The last part of this statement is the overloaded operators body
           which adds the parameter passed to the member object. */
        int operator() (int myArgument) { return myObject + myArgument; }

    private: 
        int myObject; //Our private member object.
}; 

Se tudo isso é impreciso ou simplesmente errado, fique à vontade para me corrigir!


Pouca adição. Você pode usar boost::function , para criar functores a partir de funções e métodos, assim:

class Foo
{
public:
    void operator () (int i) { printf("Foo %d", i); }
};
void Bar(int i) { printf("Bar %d", i); }
Foo foo;
boost::function<void (int)> f(foo);//wrap functor
f(1);//prints "Foo 1"
boost::function<void (int)> b(&Bar);//wrap normal function
b(1);//prints "Bar 1"

e você pode usar boost :: bind para adicionar o estado a este functor

boost::function<void ()> f1 = boost::bind(foo, 2);
f1();//no more argument, function argument stored in f1
//and this print "Foo 2" (:
//and normal function
boost::function<void ()> b1 = boost::bind(&Bar, 2);
b1();// print "Bar 2"

e mais útil, com boost :: bind e boost :: function você pode criar o functor do método de classe, na verdade este é um delegado:

class SomeClass
{
    std::string state_;
public:
    SomeClass(const char* s) : state_(s) {}

    void method( std::string param )
    {
        std::cout << state_ << param << std::endl;
    }
};
SomeClass *inst = new SomeClass("Hi, i am ");
boost::function< void (std::string) > callback;
callback = boost::bind(&SomeClass::method, inst, _1);//create delegate
//_1 is a placeholder it holds plase for parameter
callback("useless");//prints "Hi, i am useless"

Você pode criar lista ou vetor de functores

std::list< boost::function<void (EventArg e)> > events;
//add some events
....
//call them
std::for_each(
        events.begin(), events.end(), 
        boost::bind( boost::apply<void>(), _1, e));

Há um problema com todas essas coisas, mensagens de erro do compilador não são legíveis para humanos :)


Um functor é basicamente uma classe que define o operador (). Isso permite criar objetos que "pareçam" uma função:

// this is a functor
struct add_x {
  add_x(int x) : x(x) {}
  int operator()(int y) const { return x + y; }

private:
  int x;
};

// Now you can use it like this:
add_x add42(42); // create an instance of the functor class
int i = add42(8); // and "call" it
assert(i == 50); // and it added 42 to its argument

std::vector<int> in; // assume this contains a bunch of values)
std::vector<int> out(in.size());
// Pass a functor to std::transform, which calls the functor on every element 
// in the input sequence, and stores the result to the output sequence
std::transform(in.begin(), in.end(), out.begin(), add_x(1)); 
assert(out[i] == in[i] + 1); // for all i

Há algumas coisas legais sobre functores. Uma é que, diferentemente das funções regulares, elas podem conter estado. O exemplo acima cria uma função que adiciona 42 ao que você lhe der. Mas esse valor 42 não é codificado, ele foi especificado como um argumento de construtor quando criamos nossa instância de functor. Eu poderia criar outro somador, que adicionou 27, apenas chamando o construtor com um valor diferente. Isso os torna bem personalizáveis.

Como as últimas linhas mostram, você freqüentemente passa functores como argumentos para outras funções como std :: transform ou outros algoritmos de biblioteca padrão. Você poderia fazer o mesmo com um ponteiro de função regular, exceto, como eu disse acima, functores podem ser "personalizados" porque eles contêm estado, tornando-os mais flexíveis (se eu quisesse usar um ponteiro de função, eu teria que escrever uma função que acrescentou exatamente 1 ao seu argumento.O functor é geral, e adiciona o que você inicializou com ele, e eles também são potencialmente mais eficientes. No exemplo acima, o compilador sabe exatamente qual função std::transform deve chamar. Deve chamar add_x::operator() . Isso significa que pode inline essa chamada de função. E isso faz com que seja tão eficiente quanto se eu tivesse chamado manualmente a função em cada valor do vetor.

Se eu tivesse passado um ponteiro de função em vez disso, o compilador não poderia ver imediatamente a função para a qual aponta, portanto, a menos que realize algumas otimizações globais bastante complexas, ele teria que desreferenciar o ponteiro no tempo de execução e fazer a chamada.


Um functor é uma função de ordem superior que aplica uma função aos tipos parametrizados (ou seja, modelos). É uma generalização da função de ordem superior do map . Por exemplo, podemos definir um functor para std::vector assim:

template<class F, class T, class U=decltype(std::declval<F>()(std::declval<T>()))>
std::vector<U> fmap(F f, const std::vector<T>& vec)
{
    std::vector<U> result;
    std::transform(vec.begin(), vec.end(), std::back_inserter(result), f);
    return result;
}

Esta função pega um std::vector<T> e retorna std::vector<U> quando recebe uma função F que recebe um T e retorna um U Um functor não precisa ser definido em tipos de contêineres, ele pode ser definido para qualquer tipo de modelo também, incluindo std::shared_ptr :

template<class F, class T, class U=decltype(std::declval<F>()(std::declval<T>()))>
std::shared_ptr<U> fmap(F f, const std::shared_ptr<T>& p)
{
    if (p == nullptr) return nullptr;
    else return std::shared_ptr<U>(new U(f(*p)));
}

Aqui está um exemplo simples que converte o tipo para um double :

double to_double(int x)
{
    return x;
}

std::shared_ptr<int> i(new int(3));
std::shared_ptr<double> d = fmap(to_double, i);

std::vector<int> is = { 1, 2, 3 };
std::vector<double> ds = fmap(to_double, is);

Existem duas leis que os functores devem seguir. A primeira é a lei de identidade, que afirma que se o functor recebe uma função de identidade, deve ser o mesmo que aplicar a função de identidade ao tipo, ou seja, fmap(identity, x) deve ser o mesmo que identity(x) :

struct identity_f
{
    template<class T>
    T operator()(T x) const
    {
        return x;
    }
};
identity_f identity = {};

std::vector<int> is = { 1, 2, 3 };
// These two statements should be equivalent.
// is1 should equal is2
std::vector<int> is1 = fmap(identity, is);
std::vector<int> is2 = identity(is);

A lei seguinte é a lei de composição, que afirma que se o functor receber uma composição de duas funções, deve ser o mesmo que aplicar o functor para a primeira função e depois novamente para a segunda função. Então, fmap(std::bind(f, std::bind(g, _1)), x) deve ser o mesmo que fmap(f, fmap(g, x)) :

double to_double(int x)
{
    return x;
}

struct foo
{
    double x;
};

foo to_foo(double x)
{
    foo r;
    r.x = x;
    return r;
}

std::vector<int> is = { 1, 2, 3 };
// These two statements should be equivalent.
// is1 should equal is2
std::vector<foo> is1 = fmap(std::bind(to_foo, std::bind(to_double, _1)), is);
std::vector<foo> is2 = fmap(to_foo, fmap(to_double, is));




function-call-operator