2011-03-14 10 views
10

Las cargas de bibliotecas C++, el estándar incluido, le permiten adaptar sus objetos para su uso en las bibliotecas. La elección suele ser entre una función miembro o una función libre en el mismo espacio de nombres.La mecánica de la extensión a través de funciones gratuitas o funciones miembro

Me gustaría saber qué mecanismos y construcciones utiliza el código de la biblioteca para enviar una llamada que invocará una de estas funciones de "extensión", sé que esta decisión debe tomar lugar durante el tiempo de compilación e implica plantillas. El siguiente psuedocode en tiempo de ejecución no es posible/sin sentido, los motivos están fuera del alcance de esta pregunta.

if Class A has member function with signature FunctionSignature 
    choose &A.functionSignature(...) 
else if NamespaceOfClassA has free function freeFunctionSignature 
    choose freeFunctionSignature(...) 
else 
    throw "no valid extension function was provided" 

El código anterior parece el código de tiempo de ejecución: /. Entonces, ¿cómo averigua la biblioteca el espacio de nombres en el que se encuentra una clase, cómo detecta las tres condiciones, qué otras trampas hay que evitar?

La motivación de mi pregunta es que pueda encontrar los bloques de distribución en las bibliotecas y poder usar los constructos en mi propio código. Por lo tanto, las respuestas detalladas ayudarán.

!! TO WIN BOUNTY !!

Ok, según la respuesta de Steve (y los comentarios) ADL y SFINAE son los constructos clave para cablear el envío en tiempo de compilación. Tengo mi cabeza alrededor de ADL (primitivamente) y SFINAE (otra vez rudemental). Pero no sé cómo se entrenan juntos de la manera que creo que deberían.

Quiero ver un ejemplo ilustrativo de cómo estas dos construcciones se pueden juntar para que una biblioteca pueda elegir en tiempo de compilación si llamar a una función miembro proporcionada por el usuario en un objeto, o una función gratuita suministrada por el usuario en el espacio de nombres del mismo objeto. Esto solo debe hacerse utilizando las dos construcciones anteriores, sin despacho en tiempo de ejecución de ningún tipo.

Digamos que el objeto en cuestión se llama NS::Car, y este objeto debe proporcionar el comportamiento de MoveForward(int units), como una función miembro de c. Si el comportamiento se recogerá del espacio de nombres del objeto, probablemente se verá como MoveForward(const Car & car_, int units). Vamos a definir la función que desea enviar mover(NS::direction d, const NS::vehicle & v_), donde la dirección es una enumeración, y v_ es una clase base de NS::car.

+0

No puede anular 'operator <<' en su clase para dar salida a una secuencia. Un operador miembro debe tener la clase a la izquierda del operador, no a la derecha. Además, la búsqueda de nombre es parte del compilador, no de la biblioteca, e involucrará plantillas solo si hay una clase de plantilla o función de plantilla involucrada. Si está preguntando cómo se realiza la búsqueda de nombres, aclare su pregunta. De lo contrario, no sé lo que estás preguntando. –

+0

@david Sí, me equivoqué con 'operator <<' Eliminé el ejemplo, pero alguien estaba editando mi pregunta y sobrescribió la eliminación: D –

Respuesta

2

Bueno, puedo decirle cómo detectar la presencia de funciones miembro de un cierto nombre (y firma) en tiempo de compilación.Un amigo mío lo describe aquí:

Detecting the Existence of Member Functions at Compile-Time

Sin embargo, eso no va a llegar a donde quiere ir, porque sólo funciona para el tipo estático. Como desea pasar una "referencia al vehículo", no hay forma de comprobar si el tipo dinámico (el tipo del objeto concreto detrás de la referencia) tiene dicha función de miembro.

Sin embargo, si te conformas con el tipo estático, hay otra manera de hacer algo muy similar. Implementa "si el usuario proporciona una función libre sobrecargada, llámelo, de lo contrario, intente llamar a la función de miembro". Y dice así:

namespace your_ns { 

template <class T> 
void your_function(T const& t) 
{ 
    the_operation(t); // unqualified call to free function 
} 

// in the same namespace, you provide the "default" 
// for the_operation as a template, and have it call the member function: 

template <class T> 
void the_operation(T const& t) 
{ 
    t.the_operation(); 
} 

} // namespace your_ns 

De esta manera el usuario puede proporcionar su propia sobrecarga de "the_operation", en el mismo espacio de nombres como su clase, por lo que se encuentra por ADL. Por supuesto, la "operación" del usuario debe ser "más especializada" que su implementación predeterminada ; de lo contrario, la llamada sería ambigua. En la práctica, eso no es un problema, ya que todo lo que restrinja el tipo del parámetro más de lo que es una referencia-a-const a cualquier cosa será "más especializado".

Ejemplo:

namespace users_ns { 

class foo {}; 

void the_operation(foo const& f) 
{ 
    std::cout << "foo\n"; 
} 

template <class T> 
class bar {}; 

template <class T> 
void the_operation(bar<T> const& b) 
{ 
    std::cout << "bar\n"; 
} 

} // namespace users_ns 

EDIT: después de leer la respuesta de Steve Jessop de nuevo, me doy cuenta de que eso es básicamente lo que escribió, sólo que con más palabras :)

+0

Más palabras y demostración explícita del concepto en código. +1 para ese – Novelocrat

+0

gracias amigo, estaba realmente interesado en el caso del tipo estático. ver el código escrito obtiene el efecto "aha": D –

8

La biblioteca no hace nada de esto en tiempo de ejecución, el envío lo realiza el compilador cuando se compila el código de llamada. Las funciones gratuitas en el mismo espacio de nombres como uno de los argumentos se encuentran de acuerdo con las reglas de un mecanismo llamado "Argument-Dependent Lookup" (ADL), a veces llamado "Koenig lookup".

En los casos donde tiene la opción de implementar una función gratuita o una función de miembro, puede deberse a que la biblioteca proporciona una plantilla para una función gratuita que llama a la función miembro. Entonces, si su objeto proporciona una función del mismo nombre por ADL, será una mejor coincidencia que crear una instancia de la plantilla, y por lo tanto se elegirá primero. Como dice Space_C0wb0y, pueden usar SFINAE para detectar la función miembro en la plantilla y hacer algo diferente según exista o no.

No puede cambiar el comportamiento de std::cout << x; agregando una función de miembro a x, por lo que no estoy muy seguro de lo que quiere decir allí.

+0

El otro mecanismo importante para el envío es [SFINAE] (http: //en.wikipedia .org/wiki/Substitution_failure_is_not_an_error). –

+0

gracias por los indicadores, esto debería ayudarme a resolver las cosas. Además, tenías razón sobre la semántica de la secuencia, y he eliminado el ejemplo. El último código de este tipo que he examinado fue boost :: serialization, que te permite proporcionar ambos tipos de funciones. –

0

altought, a veces, los desarrolladores pueden usar las funciones de forma gratuita o funciones de clase, indistintamente, hay algunas situaciones, para usar el uno al otro.

(1) Funciones de objetos/clase ("métodos), están preferido cuando la mayor parte de su purpouse afectan sólo el objeto, o los objetos se inteded para componer otros objetos.

// object method 
MyListObject.add(MyItemObject); 
MyListObject.add(MyItemObject); 
MyListObject.add(MyItemObject); 

(2) libre (" global "o" módulo ") son preferidas, cuando involucran varios objetos, y los objetos no son parte/compuestos el uno del otro. O cuando la función usa datos simples (estructuras sin métodos, tipos primitivos)

MyStringNamespace.MyStringClass A = new MyStringNamespace.MyStringClass("Mercury"); 
MyStringNamespace.MyStringClass B = new MyStringNamespace.MyStringClass("Jupiter"); 
// free function 
bool X = MyStringNamespace.AreEqual(A, B); 

Cuando algunos objetos de acceso a funciones de módulo comunes, en C++, tiene th e "palabra clave amiga" que les permite acceder a los métodos de los objetos, sin tener en cuenta el alcance.

class MyStringClass { 
    private: 
    // ... 
    protected: 
    // ... 
    // not a method, but declared, to allow access 
    friend: 
    bool AreEqual(MyStringClass A, MyStringClass B); 
} 

bool AreEqual(MyStringClass A, MyStringClass B) { ... } 

En "orientados a objetos casi puros" lenguajes de programación como Java o C#, donde no se puede tener funciones gratuitas, funciones gratuitas se sustituyen con los métodos estáticos, lo que hace las cosas más complicadas.

1

Si lo que buscas es un ejemplo concreto, tenga en cuenta lo siguiente:

#include <cassert> 
#include <type_traits> 
#include <iostream> 

namespace NS 
{ 
    enum direction { forward, backward, left, right }; 

    struct vehicle { virtual ~vehicle() { } }; 

    struct Car : vehicle 
    { 
     void MoveForward(int units) // (1) 
     { 
      std::cout << "in NS::Car::MoveForward(int)\n"; 
     } 
    }; 

    void MoveForward(Car& car_, int units) 
    { 
     std::cout << "in NS::MoveForward(Car&, int)\n"; 
    } 
} 

template<typename V> 
class HasMoveForwardMember // (2) 
{ 
    template<typename U, void(U::*)(int) = &U::MoveForward> 
    struct sfinae_impl { }; 

    typedef char true_t; 
    struct false_t { true_t f[2]; }; 

    static V* make(); 

    template<typename U> 
    static true_t check(U*, sfinae_impl<U>* = 0); 
    static false_t check(...); 

public: 
    static bool const value = sizeof(check(make())) == sizeof(true_t); 
}; 

template<typename V, bool HasMember = HasMoveForwardMember<V>::value> 
struct MoveForwardDispatcher // (3) 
{ 
    static void MoveForward(V& v_, int units) { v_.MoveForward(units); } 
}; 

template<typename V> 
struct MoveForwardDispatcher<V, false> // (3) 
{ 
    static void MoveForward(V& v_, int units) { NS::MoveForward(v_, units); } 
}; 

template<typename V> 
typename std::enable_if<std::is_base_of<NS::vehicle, V>::value>::type // (4) 
mover(NS::direction d, V& v_) 
{ 
    switch (d) 
    { 
    case NS::forward: 
     MoveForwardDispatcher<V>::MoveForward(v_, 1); // (5) 
     break; 
    case NS::backward: 
     // ... 
     break; 
    case NS::left: 
     // ... 
     break; 
    case NS::right: 
     // ... 
     break; 
    default: 
     assert(false); 
    } 
} 

struct NonVehicleWithMoveForward { void MoveForward(int) { } }; // (6) 

int main() 
{ 
    NS::Car v; // (7) 
    //NonVehicleWithMoveForward v; // (8) 
    mover(NS::forward, v); 
} 

HasMoveForwardMember(2) es una metafunción que comprueba la existencia de una función miembro de ese nombre con la firma void(V::*)(int) en una clase dada V.MoveForwardDispatcher(3) usa esta información para llamar a la función de miembro si existe o vuelve a llamar a una función gratuita si no lo hace. mover simplemente delega la invocación de MoveForward a MoveForwardDispatcher(5).

El código como-publicado invocará Car::MoveForward(1), pero si se elimina esta función de miembro, cambiado de nombre o ha cambiado su firma, NS::MoveForward se llamará en su lugar.

También tenga en cuenta que debido a mover es una plantilla, un cheque SFINAE debe ser puesto en su lugar para conservar la semántica de solamente permitiendo objetos derivados de NS::vehicle ser pasado por v_(4). Para demostrar, si se comenta a cabo (7) y uncomments (8), mover serán llamados con un objeto de tipo NonVehicleWithMoveForward(6), lo que queremos prohibir a pesar de que HasMoveForwardMember<NonVehicleWithMoveForward>::value == true.

(Nota:. Si su biblioteca estándar no viene con std::enable_if y std::is_base_of, utilice el std::tr1:: o boost:: variantes en lugar de estar disponible)

La forma en que este tipo de código se utiliza por lo general es llamar siempre la función libre, e implemente la función gratuita en términos de algo como MoveForwardDispatcher tal que la función libre simplemente llama a la función miembro del objeto pasado en caso de que exista, sin tener que escribir sobrecargas de esa función libre para cada tipo posible que pueda tener un miembro apropiado función.

0

Si entendí correctamente su problema simplemente se resuelve utilizando herencia (tal vez múltiple). Usted tiene alguna parte una función libre de espacio de nombres:

namespace NS { 
void DoSomething() 
{ 
    std::cout << "NS::DoSomething()" << std::endl; 
} 
} // namespace NS 

utilizar una clase base que transmite la misma función:

struct SomethingBase 
{ 
    void DoSomething() 
    { 
     return NS::DoSomething(); 
    } 
}; 

Si alguna clase A se derivan de SomethingBase no implementa HacerAlgo() llamándolo llamará SomethingBase :: HacerAlgo() -> NS :: HacerAlgo():

struct A : public SomethingBase // probably other bases 
{ 
    void DoSomethingElse() 
    { 
     std::cout << "A::DoSomethingElse()" << std::endl; 
    } 
}; 

Si otra clase B se deriva de SomethingBase implementar HacerAlgo() calificó la llamada a B :: HacerAlgo():

struct B : public SomethingBase // probably other bases 

{ 
    void DoSomething() 
    { 
     std::cout << "B::DoSomething()" << std::endl; 
    } 
}; 

Así llamando HacerAlgo() en un objeto derivado de SomethingBase ejecutará el miembro si existe, o la función libre de otra manera. Tenga en cuenta que no hay nada que arrojar, se obtiene un error de compilación si no hay coincidencia con su llamada.

int main() 
{ 
    A a; 
    B b; 
    a.DoSomething(); // "NS::DoSomething()" 
    b.DoSomething(); // "B::DoSomething()" 
    a.DoSomethingElse(); // "A::DoSomethingElse()" 
    b.DoSomethingElse(); // error 'DoSomethingElse' : is not a member of 'B' 
} 
Cuestiones relacionadas