Pregunta Función de enganche en C ++?


Con "enganchar" me refiero a la capacidad de anular de forma no intrusiva el comportamiento de una función. Algunos ejemplos:

  • Imprima un mensaje de registro antes y / o después del cuerpo de la función.
  • Envuelva el cuerpo de la función en un cuerpo de captura de prueba.
  • Medir la duración de una función
  • etc ...

He visto diferentes implementaciones en varios lenguajes de programación y bibliotecas:

  • Programación Orientada a Aspectos
  • Funciones de primera clase de JavaScript
  • Patrón decorador OOP
  • Subclases de WinAPI
  • Ruby method_missing
  • TRAGOes %exception La palabra clave que está destinada a ajustar todas las funciones en un bloque try / catch puede (ab) usarse con el propósito de enganchar

Mis preguntas son:

  • IMO esta es una característica increíblemente útil que me pregunto por qué nunca se ha implementado como una característica de lenguaje C ++. ¿Hay alguna razón que impida que esto sea posible?
  • ¿Cuáles son algunas de las técnicas o bibliotecas recomendadas para implementar esto en un programa C ++?

32
2017-10-12 17:03


origen


Respuestas:


Si está hablando de hacer llamar a un nuevo método antes / después de un cuerpo de función, sin cambiar el cuerpo de la función, puede basarlo en esta, que usa una costumbre shared_ptr eliminar para desencadenar la función después del cuerpo. No puede usarse para try/catch, ya que el antes y el después deben ser funciones separadas usando esta técnica.

Además, la versión a continuación usa shared_ptr, pero con C ++ 11 deberías poder usar unique_ptr para obtener el mismo efecto sin el costo de crear y destruir un puntero compartido cada vez que lo use.

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

13
2017-10-12 17:54



Existen funciones específicas del compilador que puede aprovechar, como las de GCC. -finstrument-functions. Otros compiladores probablemente tengan características similares. Mira esto ASI pregunta para detalles adicionales.

Otro enfoque es usar algo como Envoltura de la función de Bjarne Stroustrup técnica.


5
2017-10-12 17:55



Para responder a tu primera pregunta:

  • La mayoría de los lenguajes dinámicos tienen su method_missing constructos, PHP tiene un métodos mágicos (__call y __callStatic) y Python tiene __getattr__. Creo que la razón por la cual esto no está disponible en C ++ va en contra de la naturaleza tipada de C ++. Implementar esto en una clase significa que cualquier error tipográfico terminará llamando a esta función (¡en tiempo de ejecución!), Lo que evita detectar estos problemas en tiempo de compilación. Mezclar C ++ con pato mecanografiado no parece ser una buena idea.
  • C ++ intenta ser lo más rápido posible, por lo que las funciones de primera clase están fuera de toda duda.
  • AOP. Ahora bien, esto es más interesante, desde el punto de vista tecnológico no hay nada que impida que esto se agregue al estándar C ++ (aparte del hecho de que agregar otra capa de complejidad a un estándar ya extremadamente complejo podría no ser una buena idea). De hecho, hay compiladores que pueden mover el código, AspectC ++ es uno de ellos. Hace un año o algo así no era estable, pero parece que desde entonces lograron lanzar 1.0 con un conjunto de pruebas bastante decente, por lo que ahora puede hacer el trabajo.

Hay un par de técnicas, aquí hay una pregunta relacionada:

Emulación de CLOS: before,: after y: around en C ++.


4
2017-10-12 17:38



IMO esta es una característica increíblemente útil, ¿por qué no es una característica de lenguaje C ++? ¿Hay alguna razón que impida que esto sea posible?

C ++ el lenguaje no proporciona ningún medio para hacerlo directamente. Sin embargo, tampoco plantea ninguna restricción directa contra esto (AFAIK). Este tipo de característica es más fácil de implementar en un intérprete que en un código nativo, porque la interpretación es una pieza de software, no una instrucción de máquina de transmisión de la CPU. También podría proporcionar un intérprete de C ++ con soporte para ganchos si lo desea.

El problema es por qué la gente usa C ++. Mucha gente usa C ++ porque quieren una velocidad de ejecución pura. Para lograr ese objetivo, los compiladores envían código nativo en el formato preferido del sistema operativo e intentan codificar tanto material en el archivo ejecutable compilado. La última parte a menudo significa calcular direcciones en el tiempo de compilación / enlace. Si arreglas la dirección de una función en ese momento (o, lo que es peor, en línea el cuerpo de la función), entonces ya no hay soporte para hooks.

Dicho esto, hay son formas de enganchar barato, pero requiere extensiones de compilador y no es totalmente portátil. Raymond Chen blogueó sobre cómo parches calientes está implementado en la API de Windows. También recomienda que no se use en el código regular.


2
2017-10-12 17:42



Al menos en el marco de C ++ que uso proporciona un conjunto de clases virtuales puras

class RunManager;
class PhysicsManager;
// ...

Cada uno de los cuales definió un conjunto de acciones

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

que son NOP, pero que el usuario puede anular cuando se derivan de la clase Parent.

Combine eso con la compilación condicional (sí, lo sé "¡Yuk!") y puedes obtener lo que quieres.


1
2017-10-12 17:59



Esto no es algo de C ++, pero para lograr algunas de las cosas que menciona, he usado la variable de entorno LD_PRELOAD en los sistemas * nix. Un buen ejemplo de esta técnica en acción es el faketime biblioteca que se engancha en las funciones de tiempo.


0
2017-10-12 17:46



  1. Tiene que haber una manera de implementar la funcionalidad sin afectar el rendimiento del código que no usa la funcionalidad. C ++ está diseñado según el principio de que solo paga los costos de rendimiento de las funciones que usa. Insertar si se comprueba en cada función para comprobar si ha sido anulado sería inaceptablemente lento para muchos proyectos de C ++. En particular, hacer que funcione para que no exista un costo de rendimiento mientras se permite la compilación independiente de las funciones anuladas y anuladas será complicado. Si solo permite la anulación del tiempo de compilación, entonces es más fácil de hacer performantly (el enlazador puede ocuparse de sobreescribir direcciones), pero está comparando con ruby ​​y javascript que le permiten cambiar estas cosas en tiempo de ejecución.

  2. Porque podría subvertir el sistema de tipos. ¿Qué significa que una función sea privada o no virtual si alguien puede anular su comportamiento de todos modos?

  3. La legibilidad sufriría mucho. ¡Cualquier función puede tener su comportamiento anulado en otro lugar en el código! Mientras más contexto necesite para comprender qué es una función, más difícil será descubrir una gran base de código. Enganchar es un error, no una característica. Al menos, si es capaz de leer lo que escribió meses después, es un requisito.


0
2017-10-12 17:58