2011-02-08 19 views
7

¿Es posible tener una variable miembro, que pueda calcular el puntero al objeto contenedor desde el puntero a sí mismo (en su método)?Variable miembro de clase C++ que conoce su propio desplazamiento

Tengamos una interfaz de llamada extranjera envuelta en API de la siguiente manera:

template <typename Class, MethodId Id, typename Signature> 
class MethodProxy; 

template <typename Class, MethodId Id, typename ReturnT, typename Arg1T> 
class MethodProxy<Class, Id, ReturnT()(Arg1T) { 
    public: 
    ReturnT operator()(Class &invocant, Arg1T arg1); 
}; 

y de manera similar para otros números de argumentos de 0 a N. Para cada clase en el lado exterior, una clase de C++ se declara con cierta rasgos y esta plantilla utiliza esos rasgos (y más rasgos para los tipos de argumento) para encontrar e invocar el método extraño. Esto puede ser usado como:

Foo foo; 
MethodProxy<Foo, barId, void()(int)> bar; 
bar(foo, 5); 

Ahora lo que me gustaría hacer es definir Foo de tal manera, que puedo llamar como:

Foo foo; 
foo.bar(5); 

sin repetir la firma varias veces. (obviamente crear un miembro estático y envolver la llamada en un método es simple, a la derecha). Bueno, de hecho, eso es aún más fácil:

template <typename Class, MethodId Id, typename Signature> 
class MethodMember; 
template <typename Class, MethodId Id, typename ReturnT, typename Arg1T> 
class MethodMember<Class, Id, ReturnT()(Arg1T) { 
    MethodProxy<Class, Id, Signature> method; 
    Class &owner; 
    public: 
    MethodMember(Class &owner) : owner(owner) {} 
    ReturnT operator()(Arg1T arg1) { return method(owner, arg1); } 
}; 

Eso significa sin embargo el objeto terminará contiene muchas copias del puntero a sí mismo. Así que estoy buscando una manera de hacer que estas instancias puedan calcular el puntero del propietario desde this y algunos argumentos adicionales de la plantilla.

Estaba pensando en la línea de

template <typename Class, size_t Offset, ...> 
class Member { 
    Class *owner() { 
     return reinterpret_cast<Class *>(
      reinterpret_cast<char *>(this) - Offset); 
    } 
    ... 
}; 
class Foo { 
    Member<Foo, offsetof(Foo, member), ...> member; 
    ... 
}; 

pero esto se queja de que es Foo tipo incompleto en el punto.

Sí, sé que offsetof se supone que solo funciona para los tipos "POD", pero en la práctica para cualquier miembro no virtual, que así sea, funciona. De manera similar, he intentado pasar puntero-a- (ese) -miembro (usando clase base ficticia) en ese argumento, pero eso tampoco funciona.

Tenga en cuenta que si esto funcionó, también se podría usar para implementar propiedades de tipo C# delegando a métodos de la clase contenedora.

Sé cómo hacer los métodos de envoltura mencionados anteriormente con boost.preprocessor, pero las listas de argumentos deberían especificarse en una forma extraña. Sé cómo escribir macro para generar envoltorios genéricos a través de plantillas, pero eso probablemente dará diagnósticos pobres. También sería trivial si las llamadas pudieran verse como foo.bar()(5). Pero me gustaría saber si algún truco inteligente sería posible (además, solo ese truco ingenioso sería probablemente útil para las propiedades).

Nota: El tipo de miembro no puede estar especializado ni en un puntero de miembro ni en un desplazamiento, porque el tipo debe conocerse antes de que se pueda asignar el desplazamiento. Esto se debe a que el tipo puede afectar la alineación requerida (considere la especialización explícita/parcial).

+0

leí que en varias ocasiones, pero aún no consigue lo que quiere hacer, qué quiere una clase genérica * propiedad * que es consciente de ¿Qué posee? De ser así, ¿por qué debería saber qué lo posee? Me imagino que todo lo que una propiedad realmente necesita es la capacidad de aceptar un valor y devolver el valor. – Nim

+1

Yo tampoco lo entiendo ...¿Que estás tratando de hacer? – mfontanini

+0

@Nim: Sí, quiero una clase genérica de "propiedad" que sepa de qué se trata. Para la propiedad es necesario si el valor de la propiedad debe * calcularse *. En mi caso, sin embargo, es un funtor que necesita pasar el puntero al propietario al método subyacente. –

Respuesta

6

Hacer una pregunta es la mejor manera de cuenta la respuesta, por lo que aquí es donde tengo obtenido:

El desplazamiento no puede ser un argumento de plantilla, porque el tipo debe conocerse antes de poder calcular el desplazamiento. Entonces tiene que ser devuelto por una función del argumento. Agreguemos un tipo de etiqueta (estructura ficticia) y una función sobrecargada en Propietario o directamente en la etiqueta. De esa forma podemos definir todo lo que necesitamos en un solo lugar (usando una macro). El siguiente código compila bien con gcc 4.4.5 y grabados puntero correcta para todos los miembros:

#include <cstddef> 
#include <iostream> 

using namespace std; 

(acaba de preámbulo para que sea realmente compilar)

template <typename Owner, typename Tag> 
struct offset_aware 
{ 
    Owner *owner() 
    { 
     return reinterpret_cast<Owner *>(
      reinterpret_cast<char *>(this) - Tag::offset()); 
    } 
}; 

Esto es lo que se necesita para que el objeto conscientes de su propia compensación. Se puede agregar libremente la propiedad o el functor o algún otro código para hacerlo útil. Ahora tenemos que declarar algún material extra junto con el propio elemento, por lo que vamos a definir esta macro:

#define OFFSET_AWARE(Owner, name) \ 
    struct name ## _tag { \ 
     static ptrdiff_t offset() { \ 
      return offsetof(Owner, name); \ 
     } \ 
    }; \ 
    offset_aware<Owner, name ## _tag> name 

Esta estructura define como la etiqueta y pone en una función que devuelve el desplazamiento necesario. De lo que define el miembro de datos en sí.

Tenga en cuenta que el miembro debe ser público como se define aquí, pero podríamos agregar fácilmente una declaración de "amigo" para las propiedades privadas y de soporte de etiquetas protegidas. Ahora usémoslo.

struct foo 
{ 
    int x; 
    OFFSET_AWARE(foo, a); 
    OFFSET_AWARE(foo, b); 
    OFFSET_AWARE(foo, c); 
    int y; 
}; 

Simple, ¿verdad?

int main() 
{ 
    foo f; 

    cout << "foo f = " << &f << endl 
     << "f.a: owner = " << f.a.owner() << endl 
     << "f.b: owner = " << f.b.owner() << endl 
     << "f.c: owner = " << f.c.owner() << endl; 
    return 0; 
} 

Esto imprime el mismo valor de puntero en todas las líneas. El estándar C++ no permite que los miembros tengan un tamaño 0, pero solo tendrán el tamaño de su contenido real o 1 byte si están vacíos en comparación con 4 u 8 bytes (según la plataforma) para un puntero.

+0

Probablemente deberías notar que si alguien hereda una clase polimórfica de 'foo', esta solución se romperá. –

+0

@MarkB: ¿Lo harás? No importa de qué se deriva 'foo', el miembro sigue estando en el mismo desplazamiento de la subinstancia' foo', por lo que 'owner()' devuelve el puntero a esa subinstancia, que es exactamente donde se espera que 'foo * ' punto. –

+0

@JanHudec, es un evento brillante, media década después. Gracias. – Kumputer

0

Suponiendo que las llamadas realmente necesitan una referencia al objeto que contiene, simplemente almacene la referencia al propietario. A menos que tenga pruebas de perfiles de memoria específicos de que está causando un aumento de memoria significativo para almacenar las referencias adicionales, simplemente hágalo de la manera más obvia.

+1

¿Ha leído la pregunta? Eso es exactamente lo que hace la opción del medio y que pregunté si hay una manera de obtener sin esa referencia. –

+1

@ Jan Hudec ¿Por qué crees que las referencias adicionales son un problema real que debe corregirse? Mi primera lectura de la pregunta es que quizás no haya un problema real aquí. –

+2

Bueno, la pregunta específicamente pide una solución que no los tiene. No es una cuestión si hay un problema con ellos o no — Los excluí específicamente porque de lo contrario el asunto es trivial y no vale la pena ser discutido. –

1

Por ahora, aquí está uno solución MS-específica, aún pensando en cómo hacer que sea más general

#include <stdio.h> 

#define offs(s,m) (size_t)&(((s *)0)->m) 
#define Child(A,B,y) \ 
    __if_exists(X::y) { enum{ d_##y=offs(X,y) }; } \ 
    __if_not_exists(X::y) { enum{ d_##y=0 }; } \ 
    B<A,d_##y> y; 

template <class A, int x> 
struct B { 
    int z; 
    void f(void) { 
    printf("x=%i\n", x); 
    } 
}; 

template< class X > 
struct A { 
    int x0; 
    int x1; 
    Child(A,B,y); 
    Child(A,B,z); 
}; 

typedef A<int> A0; 

typedef A<A0> A1; 

int main(void) { 
    A1 a; 
    a.y.f(); 
    a.z.f(); 
} 
+0

El __if_exists/__ if_not_exists no debería ser necesario. Es un error si no existe de todos modos. Y esa es la única cosa específica de MS allí, ¿verdad? –

+0

Hice un pequeño ajuste (básicamente reemplazando 'offs' con' offsetof' de '', pero gcc se niega a compilarlo porque no permite la referencia a los miembros antes de que se declaren (excepto en cuerpos de métodos). De hecho visual C++ tiene que cortar algunas esquinas, porque la alineación requerida en general puede depender del argumento de la plantilla. –

+0

La idea es que la clase se defina por primera vez sin compensaciones, luego se vuelva a definir usando compensaciones de la primera instancia. Es fácil de hacer con por ejemplo, 2x #include + redefinición de macro, pero eso es inconveniente. Sin embargo, todavía no sé cómo configurar SFINAE para miembros de la clase nonexistant (redefinir operator-> o -> * parecía prometedor, pero no funcionó) Así que no funcionará sin __si_exista. – Shelwien

2

1) Hay una extensión de gcc que parecía apropiado:

enum{ d_y = __builtin_choose_expr(N,offsetof(X,y),0) }; 

Pero no funcionó como se esperaba, a pesar de que el manual dice
"la función incorporada no evalúa la expresión que no era elegido "

2) los punteros de los miembros parecían interesantes, por ej. offsetof puede definirse así:

template< class C, class T > 
int f(T C::*q) { 
    return (int)&((*(C*)0).*q); 
} 

Pero todavía no hemos encontrado una manera de convertir esto en constexpr.

3) Por ahora, aquí está otra versión:

#include <stdio.h> 

#pragma pack(1) 

template <class A, int x> 
struct B { 
    int z; 
    void f(void) { 
    printf("x=%i\n", x); 
    } 
}; 

#define STRUCT(A) template< int N=0 > struct A { 
#define CHILD(A, N, B, y) }; template<> struct A<N> : A<N-1> \ 
    { B<A<N>,sizeof(A<N-1>)> y; 
#define STREND }; 

STRUCT(A) 
    int x0; 
    int x1; 
    CHILD(A,1, B, y); 
    short x2; 
    CHILD(A,2, B, z); 
    char x3; 
STREND 

typedef A<2> A1; 

int main(void) { 
    A1 a; 
    a.y.f(); 
    a.z.f(); 
} 
+0

La expresión que no se elige no es relevante. El miembro siempre existe y se debe elegir el desplazamiento de la rama. No se puede evaluar a tiempo, es necesario usar una función (en línea). –

+0

Todavía no hay pruebas de que no exista una forma de compilación pura (de hecho, ya publiqué 2). – Shelwien

Cuestiones relacionadas