¿Cómo puedo obtener de manera confiable la dirección de un objeto cuando el operador está sobrecargado?

Considere el siguiente progtwig:

struct ghost { // ghosts like to pretend that they don't exist ghost* operator&() const volatile { return 0; } }; int main() { ghost clyde; ghost* clydes_address = &clyde; // darn; that's not clyde's address :'( } 

¿Cómo obtengo la dirección de clyde ?

Estoy buscando una solución que funcione igual de bien para todo tipo de objetos. Una solución C ++ 03 sería agradable, pero también estoy interesado en las soluciones C ++ 11. Si es posible, evitemos cualquier comportamiento específico de la implementación.

std::addressof plantilla std::addressof function de C ++ 11, pero no estoy interesado en usarla aquí: me gustaría entender cómo un implementador de la Biblioteca estándar podría implementar esta plantilla de función.

Actualización: en C ++ 11, se puede usar std::addressof lugar de boost::addressof .


Primero copiemos el código de Boost, menos el trabajo del comstackdor alrededor de los bits:

 template struct addr_impl_ref { T & v_; inline addr_impl_ref( T & v ): v_( v ) {} inline operator T& () const { return v_; } private: addr_impl_ref & operator=(const addr_impl_ref &); }; template struct addressof_impl { static inline T * f( T & v, long ) { return reinterpret_cast( &const_cast(reinterpret_cast(v))); } static inline T * f( T * v, int ) { return v; } }; template T * addressof( T & v ) { return addressof_impl::f( addr_impl_ref( v ), 0 ); } 

¿Qué sucede si pasamos una referencia a la función ?

Nota: addressof no se puede usar con un puntero para funcionar

En C ++ if void func(); se declara, entonces func es una referencia a una función que no toma ningún argumento y no devuelve ningún resultado. Esta referencia a una función se puede convertir trivialmente en un puntero a la función: de @Konstantin : de acuerdo con 13.3.3.2, tanto T & como T * son indistinguibles para las funciones. El primero es una conversión de Identidad y el segundo es la conversión de Función a Puntero con el rango “Coincidencia Exacta” (13.3.3.1.1 tabla 9).

La referencia a la función pasa a través de addr_impl_ref , hay una ambigüedad en la resolución de sobrecarga para la elección de f , que se resuelve gracias al argumento ficticio 0 , que es un int primero y podría promoverse a un long (Conversión Integral).

Por lo tanto, simplemente devuelve el puntero.

¿Qué sucede si pasamos un tipo con un operador de conversión?

Si el operador de conversión produce una T* , tenemos una ambigüedad: para f(T&,long) se requiere una promoción integral para el segundo argumento, mientras que para f(T*,int) se llama al operador de conversión la primera (gracias a @litb)

Es entonces cuando se addr_impl_ref . El Estándar C ++ exige que una secuencia de conversión contenga como máximo una conversión definida por el usuario. Al addr_impl_ref el tipo en addr_impl_ref y forzar el uso de una secuencia de conversión, “desactivamos” cualquier operador de conversión que tenga el tipo.

Por lo tanto f(T&,long) se selecciona la sobrecarga f(T&,long) (y se realiza la promoción integral).

¿Qué pasa para cualquier otro tipo?

Por lo tanto f(T&,long) se selecciona la sobrecarga f(T&,long) , porque allí el tipo no coincide con el parámetro T* .

Nota: a partir de las observaciones en el archivo sobre la compatibilidad de Borland, las matrices no se degradan a punteros, sino que se pasan por referencia.

¿Qué pasa en esta sobrecarga?

Queremos evitar la aplicación del operator& al tipo, ya que puede haber sido sobrecargado.

The Standard garantiza que reinterpret_cast se puede usar para este trabajo (ver la respuesta de @Matteo Italia: 5.2.10 / 10).

Boost agrega algunas sutilezas con const y calificadores volatile para evitar las advertencias del comstackdor (y usa adecuadamente un const_cast para eliminarlas).

  • Transmitir T& a char const volatile&
  • Pelar la const y volatile
  • Aplicar el operador & para tomar la dirección
  • Devuelto a una T*

El malabarismo const / volatile es un poco de magia negra, pero simplifica el trabajo (en lugar de proporcionar 4 sobrecargas). Tenga en cuenta que dado que T no está calificado, si pasamos un ghost const& , entonces T* es ghost const* , por lo tanto, los calificadores no se han perdido realmente.

EDITAR: la sobrecarga del puntero se usa para señalar las funciones, modifiqué algo la explicación anterior. Todavía no entiendo por qué es necesario .

La siguiente salida de ideone resume esto, de alguna manera.

Esencialmente, puede reinterpretar el objeto como referencia-a-char, tomar su dirección (no invocará la sobrecarga) y volver a colocar el puntero en un puntero de su tipo.

El código Boost.AddressOf hace exactamente eso, solo teniendo cuidado adicional de la calificación volatile y const .

El truco detrás de boost::addressof y la implementación proporcionada por @Luc Danton se basa en la magia de reinterpret_cast ; la norma establece explícitamente en §5.2.10 ¶10 que

Una expresión lvalue de tipo T1 se puede convertir al tipo “referencia a T2 ” si una expresión de tipo “puntero a T1 ” se puede convertir explícitamente al tipo “puntero a T2 ” utilizando un reinterpret_cast . Es decir, una conversión de referencia reinterpret_cast(x) tiene el mismo efecto que la conversión *reinterpret_cast(&x) con los operadores integrados & y * . El resultado es un lvalue que se refiere al mismo objeto que el valor l de origen, pero con un tipo diferente.

Ahora, esto nos permite convertir una referencia de objeto arbitraria a un char & (con una calificación cv si la referencia es cv-qualified), porque cualquier puntero se puede convertir a un char * (posiblemente calificado para char * ). Ahora que tenemos un char & , la sobrecarga del operador en el objeto ya no es relevante, y podemos obtener la dirección con el operador incorporado.

La implementación de refuerzo agrega algunos pasos para trabajar con objetos calificados por cv: el primer reinterpret_cast se hace para const volatile char & , de lo contrario, un char & cast simple no funcionaría para const y / o referencias volatile ( reinterpret_cast no puede eliminar const ). Luego, la const y la volatile se eliminan con const_cast , la dirección se toma con & y se realiza una reinterpet_cast final al tipo “correcto”.

El const_cast es necesario para eliminar la const / volatile que podría haberse agregado a referencias no const / volátiles, pero no “daña” lo que era una referencia const / volatile en primer lugar, porque el reinterpret_cast final volverá a agregar el cv-qualification si estaba allí en primer lugar ( reinterpret_cast no puede eliminar el const pero puede agregarlo).

En cuanto al rest del código en addressof.hpp , parece que la mayor parte es para soluciones temporales. La static inline T * f( T * v, int ) parece ser necesaria solo para el comstackdor de Borland, pero su presencia introduce la necesidad de addr_impl_ref , de lo contrario, los tipos de puntero quedarían atrapados por esta segunda sobrecarga.

Editar : las diversas sobrecargas tienen una función diferente, vea @Matthieu M. excelente respuesta .

Bueno, ya no estoy seguro de esto; Debería investigar más ese código, pero ahora estoy cocinando la cena :), lo echaré un vistazo más tarde.

He visto una implementación de addressof hacer esto:

 char* start = &reinterpret_cast(clyde); ghost* pointer_to_clyde = reinterpret_cast(start); 

¡No me preguntes qué tan conforme es esto!

Eche un vistazo a boost :: addressof y su implementación.