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
T1
se 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
const
yvolatile
- 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.