33 votes

Accrochage de fonctions en C++ ?

Par "hooking", j'entends la possibilité de remplacer de manière non intrusive le comportement d'une fonction. Quelques exemples :

  • Imprimer un message de journal avant et/ou après le corps de la fonction.
  • Enveloppez le corps de la fonction dans un corps try catch.
  • Mesure de la durée d'une fonction
  • etc...

J'ai vu différentes implémentations dans divers langages de programmation et bibliothèques :

  • Programmation orientée aspect
  • Les fonctions de première classe de JavaScript
  • Modèle de décorateur OOP
  • Sous-classement de WinAPI
  • Ruby's method_missing
  • SWIG 's %exception qui a pour but d'envelopper toutes les fonctions dans un bloc try/catch, peut être (ab)utilisé dans le but d'accrocher

Mes questions sont les suivantes :

  • C'est une fonctionnalité tellement utile que je me demande pourquoi elle n'a jamais été implémentée en tant que fonctionnalité du langage C++. Y a-t-il des raisons qui empêchent de la rendre possible ?
  • Quelles sont les techniques ou bibliothèques recommandées pour implémenter ceci dans un programme C++ ?

13voto

Dave S Points 11381

Si vous voulez qu'une nouvelle méthode soit appelée avant/après le corps d'une fonction, sans changer le corps de la fonction, vous pouvez vous baser sur ce qui utilise un shared_ptr pour déclencher la fonction post-corps. Il ne peut pas être utilisé pour try/catch Il s'agit d'une technique qui permet de créer des fonctions distinctes pour l'avant et l'après.

De plus, la version ci-dessous utilise shared_ptr mais avec C++11 vous devriez pouvoir utiliser unique_ptr pour obtenir le même effet sans le coût de création et de destruction d'un pointeur partagé à chaque fois que vous l'utilisez.

#include <iostream>
#include <boost/chrono/chrono.hpp>
#include <boost/chrono/system_clocks.hpp>
#include <boost/shared_ptr.hpp>

template <typename T, typename Derived>
class base_wrapper
{
protected:
  typedef T wrapped_type;

  Derived* self() {
    return static_cast<Derived*>(this);
  }

  wrapped_type* p;

  struct suffix_wrapper
  {
    Derived* d;
    suffix_wrapper(Derived* d): d(d) {};
    void operator()(wrapped_type* p)
    {
      d->suffix(p);
    }
  };
public:
  explicit base_wrapper(wrapped_type* p) :  p(p) {};

  void prefix(wrapped_type* p) {
     // Default does nothing
  };

  void suffix(wrapped_type* p) {
     // Default does nothing
  }

  boost::shared_ptr<wrapped_type> operator->() 
  {
    self()->prefix(p);
    return boost::shared_ptr<wrapped_type>(p,suffix_wrapper(self()));
  }
};

template<typename T>
class timing_wrapper : public base_wrapper< T, timing_wrapper<T> >
{
  typedef  base_wrapper< T, timing_wrapper<T> > base;
  typedef boost::chrono::time_point<boost::chrono::system_clock, boost::chrono::duration<double> > time_point;

  time_point begin;
public:
  timing_wrapper(T* p): base(p) {}

  void prefix(T* p) 
  {
    begin = boost::chrono::system_clock::now();
  }

  void suffix(T* p)
  {
    time_point end = boost::chrono::system_clock::now();

    std::cout << "Time: " << (end-begin).count() << std::endl;
  }
};

template <typename T>
class logging_wrapper : public base_wrapper< T, logging_wrapper<T> >
{
  typedef  base_wrapper< T, logging_wrapper<T> > base;
public:
  logging_wrapper(T* p): base(p) {}

  void prefix(T* p) 
  {
    std::cout << "entering" << std::endl;
  }

  void suffix(T* p) 
  {
    std::cout << "exiting" << std::endl;
  }

};

template <template <typename> class wrapper, typename T> 
wrapper<T> make_wrapper(T* p) 
{
  return wrapper<T>(p);
}

class X 
{
public:
  void f()  const
  {
    sleep(1);
  }

  void g() const
  {
    std::cout << __PRETTY_FUNCTION__ << std::endl;
  }

};

int main () {

  X x1;

  make_wrapper<timing_wrapper>(&x1)->f();

  make_wrapper<logging_wrapper>(&x1)->g();
  return 0;
}

5voto

Void Points 2583

Il existe des fonctionnalités spécifiques au compilateur dont vous pouvez tirer parti, telles que la fonction -fonctions des instruments . D'autres compilateurs auront probablement des fonctionnalités similaires. Voir ce Question pour plus de détails.

Une autre approche consiste à utiliser quelque chose comme L'emballage des fonctions de Bjarne Stroustrup technique.

4voto

Karoly Horvath Points 45145

Pour répondre à votre première question :

  • La plupart des langages dynamiques ont leur method_missing PHP dispose d'une méthodes de magie ( __call et __callStatic ) et Python a __getattr__ . Je pense que la raison pour laquelle cela n'est pas disponible en C++ est que cela va à l'encontre de la nature typée du C++. L'implémentation de cette fonction dans une classe signifie que toute erreur de typage finira par appeler cette fonction (au moment de l'exécution !), ce qui empêche de détecter ces problèmes au moment de la compilation. Mélanger le C++ avec le typage des canards ne semble pas être une bonne idée.
  • Le C++ essaie d'être aussi rapide que possible, donc les fonctions de première classe sont hors de question.
  • AOP. Maintenant, c'est plus intéressant, techniquement, il n'y a rien qui empêche que cela soit ajouté à la norme C++ (en dehors du fait qu'ajouter une autre couche de complexité à une norme déjà extrêmement complexe n'est peut-être pas une bonne idée). En fait, il existe des compilateurs capables d'onduler le code, AspectC++ est l'un d'entre eux. Il y a un an environ, il n'était pas stable, mais il semble que depuis, ils ont réussi à publier la version 1.0 avec une suite de tests assez décente, ce qui fait qu'il pourrait faire l'affaire maintenant.

Il existe plusieurs techniques. Voici une question connexe :

Émulation de CLOS :before, :after et :around en C++ .

2voto

André Caron Points 19543

C'est une fonctionnalité incroyablement utile, alors pourquoi n'est-ce pas une fonctionnalité du langage C++ ? Y a-t-il des raisons qui empêchent de la rendre possible ?

C++ la langue ne fournit aucun moyen de le faire directement. Cependant, il ne pose pas non plus de contrainte directe contre cela (AFAIK). Ce type de fonctionnalité est plus facile à mettre en œuvre dans un interpréteur que dans un code natif, parce que l'interpréteur est un logiciel, et non un processeur diffusant des instructions machine. Vous pourriez très bien fournir un interpréteur C++ avec le support des hooks si vous le souhaitiez.

Le problème est que pourquoi les gens utilisent C++. Beaucoup de gens utilisent le C++ parce qu'ils veulent une vitesse d'exécution pure et simple. Pour atteindre cet objectif, les compilateurs produisent du code natif dans le format préféré du système d'exploitation et essaient de coder en dur un maximum de choses dans le fichier exécutable compilé. La dernière partie signifie souvent le calcul des adresses au moment de la compilation ou de la liaison. Si l'on fixe l'adresse d'une fonction à ce moment-là (ou pire, si l'on met en ligne le corps de la fonction), il n'y a plus de support pour les hooks.

Ceci étant dit, il y a sont des moyens de rendre le hooking bon marché, mais cela nécessite des extensions de compilateur et n'est absolument pas portable. Raymond Chen a expliqué sur son blog comment chaud Parcheando est mis en œuvre dans l'API Windows. Il recommande également de ne pas l'utiliser dans du code ordinaire.

1voto

dmckee Points 50318

Au moins le framework c++ que j'utilise fournit un ensemble de classes virtuelles pures.

class RunManager;
class PhysicsManager;
// ...

Chacun d'entre eux a défini un ensemble d'actions

void PreRunAction();
void RunStartAction()
void RunStopAction();
void PostRunAction();

qui sont NOPs, mais que l'utilisateur peut surcharger lorsqu'il dérive de la classe Parent.

Combinez cela avec la compilation conditionnelle (ouais, je sais "Yuk !" ) et vous pouvez obtenir ce que vous voulez.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X