Chamar métodos de classe C ++ por meio de um ponteiro de função

113

Como obtenho um ponteiro de função para uma função de membro de classe e, posteriormente, chamo essa função de membro com um objeto específico? Eu gostaria de escrever:

class Dog : Animal
{
    Dog ();
    void bark ();
}


Dog* pDog = new Dog ();
BarkFunction pBark = &Dog::bark;
(*pBark) (pDog);

Além disso, se possível, gostaria de invocar o construtor por meio de um ponteiro:

NewAnimalFunction pNew = &Dog::Dog;
Animal* pAnimal = (*pNew)();    

Isso é possível e, em caso afirmativo, qual é a forma preferida de fazer isso?

Tony o Pônei
fonte
1
Eu ainda não entendo 'por que' se você deseja chamar uma função de membro de objetos e simplesmente passar um ponteiro para o objeto? Se as pessoas reclamam disso porque permite que você encapsule melhor a classe, por que não fazer uma classe de interface da qual todas as classes herdam?
Chade,
Pode ser útil na implementação de algo como o padrão de comando, embora muitas pessoas usem boost :: function para ocultar a mecânica de ponteiro de membro bruto.
CB Bailey
9
Por que você aloca aquele cachorro dinamicamente? Em seguida, você também deve excluir manualmente o objeto. Parece que você está vindo de Java, C # ou alguma outra linguagem comparável e ainda luta com C ++. Um objeto simples e automático ( Dog dog;) é mais provável que você queira.
sbi
1
@Chad: Eu concordaria na maioria das vezes, mas há momentos em que passar uma referência seria mais caro. Considere um loop que está iterando sobre algum tipo de dados (análise, cálculo, etc.) do que ser capaz de chamar uma função com base em alguns cálculos if / else impõe um custo onde apenas chamar a função apontada poderia evitar tal if / then / else verifica se essas verificações podem ser feitas antes de entrar no loop.
Eric,

Respostas:

126

Leia este para o detalhe:

// 1 define a function pointer and initialize to NULL

int (TMyClass::*pt2ConstMember)(float, char, char) const = NULL;

// C++

class TMyClass
{
public:
   int DoIt(float a, char b, char c){ cout << "TMyClass::DoIt"<< endl; return a+b+c;};
   int DoMore(float a, char b, char c) const
         { cout << "TMyClass::DoMore" << endl; return a-b+c; };

   /* more of TMyClass */
};
pt2ConstMember = &TMyClass::DoIt; // note: <pt2Member> may also legally point to &DoMore

// Calling Function using Function Pointer

(*this.*pt2ConstMember)(12, 'a', 'b');
Satbir
fonte
23
Surpreendente que eles decidiram que isso: *this.*pt2Memberiria funcionar. *tem maior precedência sobre .*... Pessoalmente, eu ainda teria escrito this->*pt2Member, é um operador a menos.
Alexis Wilke
7
Por que você tem que inicializar pt2ConstMemberpara NULL?
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
@AlexisWilke por que é surpreendente? Para objetos diretos (não ponteiros) é (object.*method_pointer), então queremos *que tenha maior prioridade.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
@ TomášZato, se não me engano (e posso estar), thisestá apenas sendo usado para demonstrar que tudo o que você aplicar .*deve ser um ponteiro para uma instância da (sub) classe. No entanto, esta é uma nova sintaxe para mim, estou apenas supondo com base em outras respostas e recursos vinculados aqui. Estou sugerindo uma edição para deixar isso mais claro.
c1moore
1
Oh, não, parabéns pelo 100!
Jonathan Mee
57

Como obtenho um ponteiro de função para uma função de membro de classe e, posteriormente, chamo essa função de membro com um objeto específico?

É mais fácil começar com um typedef. Para uma função de membro, você adiciona o nome da classe na declaração de tipo:

typedef void(Dog::*BarkFunction)(void);

Então, para invocar o método, você usa o ->* operador:

(pDog->*pBark)();

Além disso, se possível, também gostaria de invocar o construtor por meio de um ponteiro. Isso é possível e, em caso afirmativo, qual é a forma preferida de fazer isso?

Não acredito que você possa trabalhar com construtores como este - ctors e dtors são especiais. A maneira normal de conseguir esse tipo de coisa seria usar um método de fábrica, que é basicamente uma função estática que chama o construtor para você. Veja o código abaixo para um exemplo.

Modifiquei seu código para fazer basicamente o que você descreve. Existem algumas advertências abaixo.

#include <iostream>

class Animal
{
public:

    typedef Animal*(*NewAnimalFunction)(void);

    virtual void makeNoise()
    {
        std::cout << "M00f!" << std::endl;
    }
};

class Dog : public Animal
{
public:

    typedef void(Dog::*BarkFunction)(void);

    typedef Dog*(*NewDogFunction)(void);

    Dog () {}

    static Dog* newDog()
    {
        return new Dog;
    }

    virtual void makeNoise ()
    {
        std::cout << "Woof!" << std::endl;
    }
};

int main(int argc, char* argv[])
{
    // Call member function via method pointer
    Dog* pDog = new Dog ();
    Dog::BarkFunction pBark = &Dog::makeNoise;

    (pDog->*pBark)();

    // Construct instance via factory method
    Dog::NewDogFunction pNew = &Dog::newDog;

    Animal* pAnimal = (*pNew)();

    pAnimal->makeNoise();

    return 0;
}

Agora, embora você possa usar normalmente um Dog*no lugar de um Animal*graças à magia do polimorfismo, o tipo de um ponteiro de função não segue as regras de pesquisa da hierarquia de classes. Portanto, um ponteiro de método Animal não é compatível com um ponteiro de método Dog, em outras palavras, você não pode atribuir a Dog* (*)()a uma variável do tipo Animal* (*)().

A estática newDog método é um exemplo simples de fábrica, que simplesmente cria e retorna novas instâncias. Por ser uma função estática, ela possui um regular typedef(sem qualificador de classe).

Tendo respondido às perguntas acima, eu me pergunto se não há uma maneira melhor de alcançar o que você precisa. Existem alguns cenários específicos em que você faria esse tipo de coisa, mas você pode descobrir que existem outros padrões que funcionam melhor para o seu problema. Se você descrever em termos mais gerais o que está tentando alcançar, a colmeia-mente pode ser ainda mais útil!

Relacionado ao acima, você sem dúvida achará a biblioteca Boost bind e outros módulos relacionados muito úteis.

gavinb
fonte
10
Eu uso C ++ há mais de 10 anos e continuo aprendendo algo novo regularmente. Eu nunca tinha ouvido falar ->*antes, mas agora espero nunca mais precisar disso :)
Thomas
31

Não acho que alguém tenha explicado aqui que um problema é que você precisa de " dicas de membros " em vez de ponteiros de função normais.

Os ponteiros de membro para funções não são simplesmente ponteiros de função. Em termos de implementação, o compilador não pode usar um endereço de função simples porque, em geral, você não sabe o endereço a ser chamado até saber para qual objeto remover a referência (pense em funções virtuais). Você também precisa conhecer o objeto para fornecer othis parâmetro implícito, é claro.

Tendo dito que você precisa deles, agora direi que você realmente precisa evitá-los. Sério, as sugestões dos membros são uma dor. É muito mais lógico olhar para padrões de projeto orientado a objetos que atingem o mesmo objetivo, ou usar um boost::functionou qualquer outro como mencionado acima - supondo que você faça essa escolha, claro.

Se você estiver fornecendo aquele ponteiro de função para o código existente, então você realmente precisa de um ponteiro de função simples, você deve escrever uma função como um membro estático da classe. Uma função de membro estático não entende this, então você precisará passar o objeto como um parâmetro explícito. Era uma vez um idioma não tão incomum ao longo dessas linhas para trabalhar com código C antigo que precisa de ponteiros de função

class myclass
{
  public:
    virtual void myrealmethod () = 0;

    static void myfunction (myclass *p);
}

void myclass::myfunction (myclass *p)
{
  p->myrealmethod ();
}

Visto que myfunctioné apenas uma função normal (questões de escopo à parte), um ponteiro de função pode ser encontrado no modo C normal.

EDIT - este tipo de método é chamado de "método de classe" ou "função de membro estático". A principal diferença de uma função não membro é que, se você fizer referência a ela de fora da classe, deverá especificar o escopo usando o ::operador de resolução de escopo. Por exemplo, para obter o ponteiro de função, use &myclass::myfunctione para chamá-lo usemyclass::myfunction (arg); .

Esse tipo de coisa é bastante comum ao usar as APIs Win32 antigas, que foram originalmente projetadas para C em vez de C ++. É claro que, nesse caso, o parâmetro normalmente é LPARAM ou semelhante, em vez de um ponteiro, e é necessária alguma conversão.

Steve314
fonte
'myfunction' não é uma função normal se por normal você se referir a uma função de estilo C. 'myfunction' é mais precisamente chamado de método de myclass. Os métodos de uma classe não são como funções normais, pois eles têm algo que uma função de estilo C não tem, que é o ponteiro 'this'.
Eric,
3
aconselhar o uso de impulso é draconiano. Existem boas razões práticas para usar ponteiros de método. Não me importo com a menção de boost como uma alternativa, mas odeio quando alguém diz que outra pessoa deveria usá-lo sem conhecer todos os fatos. O impulso tem um custo! E se esta for uma plataforma embarcada, então pode não ser uma escolha possível. Além disso, eu realmente gosto da sua escrita.
Eric,
@Eric - Em seu segundo ponto, eu não pretendia dizer "tu deverás usar o Boost" e, na verdade, nunca usei o Boost. A intenção (pelo que eu sei depois de 3 anos) era que as pessoas procurassem alternativas e listassem algumas possibilidades. "Ou qualquer coisa" indica que a lista não é exaustiva. Os ponteiros de membro têm um custo de legibilidade. Sua representação concisa de origem também pode disfarçar custos de tempo de execução - em particular, um ponteiro de membro para um método deve lidar com métodos virtuais e não virtuais e deve saber quais.
Steve314
@Eric - Não apenas isso, mas esses problemas são uma razão para a não portabilidade com ponteiros de membro - Visual C ++, pelo menos no passado, precisava de algumas dicas extras sobre como representar tipos de ponteiros de membro. Eu usaria a abordagem de função estática para um sistema embarcado - a representação de um ponteiro é a mesma que qualquer outro ponteiro de função, os custos são óbvios e não há problema de portabilidade. E a chamada encapsulada pela função de membro estático sabe (em tempo de compilação) se a chamada é virtual ou não - nenhuma verificação de tempo de execução necessária além das pesquisas usuais de vtable para métodos virtuais.
Steve314,
@Eric - em seu primeiro ponto - estou ciente de que uma função de membro estático não é exatamente o mesmo que uma função de estilo C (portanto, "questões de escopo à parte"), mas provavelmente deveria ter incluído o nome.
Steve314,
18
typedef void (Dog::*memfun)();
memfun doSomething = &Dog::bark;
....
(pDog->*doSomething)(); // if pDog is a pointer
// (pDog.*doSomething)(); // if pDog is a reference
AraK
fonte
2
Deve ser: (pDog -> * doSomething) (); // se pDog é um ponteiro // (pDog. * doSomething) (); // se pDog for uma referência, o operador as () tem maior prioridade do que -> * e. *.
Tomek
12

Exemplo de execução mínima

main.cpp

#include <cassert>

class C {
    public:
        int i;
        C(int i) : i(i) {}
        int m(int j) { return this->i + j; }
};

int main() {
    // Get a method pointer.
    int (C::*p)(int) = &C::m;

    // Create a test object.
    C c(1);
    C *cp = &c;

    // Operator .*
    assert((c.*p)(2) == 3);

    // Operator ->*
    assert((cp->*p)(2) == 3);
}

Compile e execute:

g++ -ggdb3 -O0 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out

Testado no Ubuntu 18.04.

Você não pode alterar a ordem dos parênteses ou omiti-los. O seguinte não funciona:

c.*p(2)
c.*(p)(2)

O GCC 9.2 falharia com:

main.cpp: In function int main()’:
main.cpp:19:18: error: must use ‘.*’ or ‘->*’ to call pointer-to-member function in p (...)’, e.g. ‘(... ->* p) (...)’
   19 |     assert(c.*p(2) == 3);
      |

C ++ 11 padrão

.*e ->*são operadores únicos introduzidos em C ++ para este propósito, e não presentes em C.

Rascunho do padrão C ++ 11 N3337 :

  • 2.13 "Operadores e pontuadores" tem uma lista de todos os operadores, que contém .*e->* .
  • 5.5 "Operadores de ponteiro para membro" explica o que eles fazem
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
fonte
11

Vim aqui para aprender como criar um ponteiro de função (não um ponteiro de método) a partir de um método, mas nenhuma das respostas aqui fornece uma solução. Aqui está o que eu descobri:

template <class T> struct MethodHelper;
template <class C, class Ret, class... Args> struct MethodHelper<Ret (C::*)(Args...)> {
    using T = Ret (C::*)(Args...);
    template <T m> static Ret call(C* object, Args... args) {
        return (object->*m)(args...);
    }
};

#define METHOD_FP(m) MethodHelper<decltype(m)>::call<m>

Portanto, para seu exemplo, você faria agora:

Dog dog;
using BarkFunction = void (*)(Dog*);
BarkFunction bark = METHOD_FP(&Dog::bark);
(*bark)(&dog); // or simply bark(&dog)

Editar:
usando C ++ 17, há uma solução ainda melhor:

template <auto m> struct MethodHelper;
template <class C, class Ret, class... Args, Ret (C::*m)(Args...)> struct MethodHelper<m> {
    static Ret call(C* object, Args... args) {
        return (object->*m)(args...);
    }
};

que pode ser usado diretamente sem a macro:

Dog dog;
using BarkFunction = void (*)(Dog*);
BarkFunction bark = MethodHelper<&Dog::bark>::call;
(*bark)(&dog); // or simply bark(&dog)

Para métodos com modificadores como, constvocê pode precisar de mais algumas especializações, como:

template <class C, class Ret, class... Args, Ret (C::*m)(Args...) const> struct MethodHelper<m> {
    static Ret call(const C* object, Args... args) {
        return (object->*m)(args...);
    }
};
Cílio
fonte
6

O motivo pelo qual você não pode usar ponteiros de função para chamar funções-membro é que os ponteiros de função comuns geralmente são apenas o endereço de memória da função.

Para chamar uma função de membro, você precisa saber duas coisas:

  • Qual função de membro chamar
  • Qual instância deve ser usada (cuja função-membro)

Ponteiros de função comuns não podem armazenar ambos. Ponteiros de função de membro C ++ são usados ​​para armazenar a), que é o motivo pelo qual você precisa especificar a instância explicitamente ao chamar um ponteiro de função de membro.

hrnt
fonte
1
Votei nisso, mas gostaria de acrescentar um ponto de esclarecimento caso o OP não saiba a que você está se referindo por "qual instância". Gostaria de expandir para explicar o ponteiro inerente 'this'.
Eric,
6

Um ponteiro de função para um membro de classe é um problema realmente adequado para usar boost :: function. Pequeno exemplo:

#include <boost/function.hpp>
#include <iostream>

class Dog 
{
public:
   Dog (int i) : tmp(i) {}
   void bark ()
   {
      std::cout << "woof: " << tmp << std::endl;
   }
private:
   int tmp;
};



int main()
{
   Dog* pDog1 = new Dog (1);
   Dog* pDog2 = new Dog (2);

   //BarkFunction pBark = &Dog::bark;
   boost::function<void (Dog*)> f1 = &Dog::bark;

   f1(pDog1);
   f1(pDog2);
}
Benjamin
fonte
2

Para criar um novo objeto, você pode usar o novo posicionamento, como mencionado acima, ou fazer com que sua classe implemente um método clone () que cria uma cópia do objeto. Você pode então chamar esse método clone usando um ponteiro de função de membro, conforme explicado acima, para criar novas instâncias do objeto. A vantagem do clone é que às vezes você pode estar trabalhando com um ponteiro para uma classe base onde você não conhece o tipo do objeto. Nesse caso, um método clone () pode ser mais fácil de usar. Além disso, clone () permitirá que você copie o estado do objeto se for o que você deseja.

Corwin Joy
fonte
os clones podem ser caros e o OP pode querer evitá-los se o desempenho for um problema ou alguma preocupação.
Eric,
0

Fiz isso com std :: function e std :: bind ..

Eu escrevi esta classe EventManager que armazena um vetor de manipuladores em um unordered_map que mapeia tipos de eventos (que são apenas const unsigned int, eu tenho um grande enum com escopo de namespace deles) para um vetor de manipuladores para esse tipo de evento.

Na minha classe EventManagerTests, configurei um manipulador de eventos, como este:

auto delegate = std::bind(&EventManagerTests::OnKeyDown, this, std::placeholders::_1);
event_manager.AddEventListener(kEventKeyDown, delegate);

Esta é a função AddEventListener:

std::vector<EventHandler>::iterator EventManager::AddEventListener(EventType _event_type, EventHandler _handler)
{
    if (listeners_.count(_event_type) == 0) 
    {
        listeners_.emplace(_event_type, new std::vector<EventHandler>());
    }
    std::vector<EventHandler>::iterator it = listeners_[_event_type]->end();
    listeners_[_event_type]->push_back(_handler);       
    return it;
}

Aqui está a definição do tipo EventHandler:

typedef std::function<void(Event *)> EventHandler;

Então, de volta a EventManagerTests :: RaiseEvent, eu faço o seguinte:

Engine::KeyDownEvent event(39);
event_manager.RaiseEvent(1, (Engine::Event*) & event);

Este é o código para EventManager :: RaiseEvent:

void EventManager::RaiseEvent(EventType _event_type, Event * _event)
{
    if (listeners_.count(_event_type) > 0)
    {
        std::vector<EventHandler> * vec = listeners_[_event_type];
        std::for_each(
            begin(*vec), 
            end(*vec), 
            [_event](EventHandler handler) mutable 
            {
                (handler)(_event);
            }
        );
    }
}

Isso funciona. Eu recebo a chamada em EventManagerTests :: OnKeyDown. Tenho que deletar os vetores na hora de limpar, mas depois que faço isso não há vazamentos. Gerar um evento leva cerca de 5 microssegundos no meu computador, que é cerca de 2008. Não é exatamente super rápido, mas. É justo, desde que eu saiba disso e não o use em código ultra quente.

Eu gostaria de acelerá-lo rolando meu próprio std :: function e std :: bind, e talvez usando um array de matrizes em vez de um unordered_map de vetores, mas ainda não descobri como armazenar uma função de membro ponteiro e chamá-lo do código que não sabe nada sobre a classe que está sendo chamada. A resposta de Eyelash parece muito interessante ..

Shavais
fonte