c++ - que - ¿Cómo puedo obtener de manera confiable la dirección de un objeto cuando el operador está sobrecargado?
sobrecarga de operadores c++ (5)
Considere el siguiente programa:
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.
Eche un vistazo a boost::addressof y su implementación.
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
T1se puede convertir al tipo "referencia aT2" si una expresión de tipo "puntero aT1" se puede convertir explícitamente al tipo "puntero aT2" utilizando unreinterpret_cast. Es decir, una conversión de referenciareinterpret_cast<T&>(x)tiene el mismo efecto que la conversión*reinterpret_cast<T*>(&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 resto 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 compilador 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.
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 .
He visto una implementación de addressof hacer esto:
char* start = &reinterpret_cast<char&>(clyde);
ghost* pointer_to_clyde = reinterpret_cast<ghost*>(start);
¡No me preguntes qué tan conforme es esto!
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 compilador alrededor de los bits:
template<class T>
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<class T>
struct addressof_impl
{
static inline T * f( T & v, long ) {
return reinterpret_cast<T*>(
&const_cast<char&>(reinterpret_cast<const volatile char &>(v)));
}
static inline T * f( T * v, int ) { return v; }
};
template<class T>
T * addressof( T & v ) {
return addressof_impl<T>::f( addr_impl_ref<T>( 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 compilador (y usa adecuadamente un const_cast para eliminarlas).
- Transmitir
T&achar const volatile& - Pelar la
constyvolatile - Aplicar el operador
¶ 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.