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

170

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 clydela dirección?

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

Soy consciente de la std::addressofplantilla de función 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.

James McNellis
fuente
41
@jalf: Esa estrategia es aceptable, pero ahora que he golpeado a dichos individuos en la cabeza, ¿cómo puedo evitar su código abominable? :-)
James McNellis
55
@jalf Uhm, a veces necesitas sobrecargar este operador y devolver un objeto proxy. Aunque no puedo pensar en un ejemplo en este momento.
Konrad Rudolph
55
@ Konrad: yo tampoco. Si lo necesita, sugeriría que una mejor opción podría ser repensar su diseño, porque sobrecargar ese operador simplemente causa demasiados problemas. :)
jalf
2
@ Konrad: En aproximadamente 20 años de programación en C ++, una vez intenté sobrecargar a ese operador. Eso fue al comienzo de esos veinte años. Ah, y no pude hacer eso utilizable. En consecuencia, la entrada de preguntas frecuentes sobre sobrecarga del operador dice "La dirección unaria del operador nunca debe sobrecargarse". Obtendrá una cerveza gratis la próxima vez que nos veamos si puede encontrar un ejemplo convincente para sobrecargar a este operador. (Sé que te vas de Berlín, así que puedo ofrecer esto con seguridad :))
sbi
55
CComPtr<>y CComQIPtr<>tener un sobrecargadooperator&
Simon Richter

Respuestas:

102

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


Copiemos primero el código de Boost, menos el trabajo del compilador alrededor de 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: addressofno se puede usar con un puntero para funcionar

En C ++ si void func();se declara, entonces funces una referencia a una función que no toma argumentos y no devuelve ningún resultado. Esta referencia a una función se puede convertir trivialmente en un puntero a la función, desde @Konstantin: De acuerdo con 13.3.3.2 a ambos, T &y no se T *pueden distinguir para las funciones. La primera es una conversión de identidad y la segunda es la conversión de función a puntero, ambas con rango de "coincidencia exacta" (13.3.3.1.1 tabla 9).

La referencia a la función pasa 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 el intprimero y podría promoverse a a long(Conversión integral).

Por lo tanto, simplemente devolvemos el puntero.

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

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

Ahí es cuando addr_impl_refentra en acción. El estándar C ++ exige que una secuencia de conversión pueda contener como máximo una conversión definida por el usuario. Al envolver el tipo addr_impl_refy forzar el uso de una secuencia de conversión, "deshabilitamos" cualquier operador de conversión con el que viene el tipo.

Así f(T&,long)se selecciona la sobrecarga (y se realiza la Promoción Integral).

¿Qué pasa con cualquier otro tipo?

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

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

¿Qué pasa en esta sobrecarga?

Queremos evitar aplicar operator&al tipo, ya que puede haber sido sobrecargado.

La Norma garantiza que reinterpret_castse puede utilizar para este trabajo (consulte la respuesta de @Matteo Italia: 5.2.10 / 10).

Boost agrega algunas sutilezas consty volatilecalificadores para evitar advertencias del compilador (y usa adecuadamente a const_castpara eliminarlos).

  • Fundido T&achar const volatile&
  • Pelar el constyvolatile
  • Solicite al &operador que tome la dirección
  • Echado de vuelta a un T*

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

EDITAR: la sobrecarga del puntero se utiliza para el puntero a las funciones, modifiqué un poco la explicación anterior. Sin embargo, todavía no entiendo por qué es necesario .

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

Matthieu M.
fuente
2
"¿Qué pasa si pasamos un puntero?" La parte es incorrecta. Si pasamos un puntero a algún tipo U, la dirección de la función se infiere que el tipo 'T' es 'U *' y addr_impl_ref tendrá dos sobrecargas: 'f (U * &, long)' y 'f (U **, int) ', obviamente se seleccionará el primero.
Konstantin Oznobihin
@ Konstantin: correcto, pensé que las dos fsobrecargas eran plantillas de funciones, mientras que son funciones miembro regulares de una clase de plantilla, gracias por señalarlo. (Ahora solo necesito averiguar cuál es el uso de la sobrecarga, ¿algún consejo?)
Matthieu M.
Esta es una gran respuesta bien explicada. Me imaginé que había algo más en esto que simplemente "transmitir char*". Gracias Matthieu
James McNellis
@James: He recibido mucha ayuda de @Konstantin que me golpeaba la cabeza con un palo cada vez que cometía un error: D
Matthieu M.
3
¿Por qué necesitaría trabajar con tipos que tienen una función de conversión? ¿No preferiría la coincidencia exacta a invocar cualquier función de conversión T*? EDITAR: Ahora veo. Lo haría, pero con el 0argumento terminaría en una encrucijada , por lo que sería ambiguo.
Johannes Schaub - litb
99

Uso std::addressof.

Puedes pensar que hace lo siguiente detrás de escena:

  1. Reinterpretar el objeto como una referencia a char
  2. Tome la dirección de eso (no llamará a la sobrecarga)
  3. Lanza el puntero a un puntero de tu tipo.

Las implementaciones existentes (incluyendo Boost.Addressof) hacen exactamente eso, solo teniendo cuidado adicional consty volatilecalificación.

Konrad Rudolph
fuente
16
Me gusta esta explicación mejor que la seleccionada, ya que se puede entender fácilmente.
Trineo
49

El truco detrás boost::addressofy 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 de tipo lvalue T1se 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 a reinterpret_cast. Es decir, un reparto de referencia reinterpret_cast<T&>(x)tiene el mismo efecto que la conversión *reinterpret_cast<T*>(&x)con los operadores integrados &y *. El resultado es un valor de l que se refiere al mismo objeto que el valor de origen, pero con un tipo diferente.

Ahora, esto nos permite convertir una referencia de objeto arbitraria a a char &(con una calificación de cv si la referencia está calificada por cv), porque cualquier puntero se puede convertir a a (posiblemente calificado por cv) 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 impulso agrega algunos pasos para trabajar con objetos calificados para cv: el primero reinterpret_castse realiza para const volatile char &, de lo contrario, un char &molde simple no funcionaría consty / o volatilereferencias ( reinterpret_castno se puede eliminar const). Luego, consty volatilese elimina con const_cast, se toma la dirección &y se realiza una final reinterpet_castpara el tipo "correcto".

Se const_castnecesita para eliminar el const/ volatileque podría haberse agregado a referencias no constantes / volátiles, pero no "daña" lo que fue un const/ volatilereferencia en primer lugar, porque el final reinterpret_castvolverá a agregar la calificación cv si era allí en primer lugar ( reinterpret_castno puede eliminar el constpero puede agregarlo).

En cuanto al resto del código addressof.hpp, parece que la mayor parte es para soluciones alternativas. El static inline T * f( T * v, int )parece ser necesario sólo para el compilador Borland, pero su presencia introduce la necesidad de que addr_impl_ref, de lo contrario los tipos de puntero serían capturados 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 tampoco; Debería investigar más ese código, pero ahora estoy cocinando la cena :), lo echaré un vistazo más tarde.

Matteo Italia
fuente
La explicación de Matthieu M. sobre pasar el puntero a addressof es incorrecta. No estropees tu gran respuesta con tales ediciones :)
Konstantin Oznobihin
"buen provecho", la investigación adicional muestra que la sobrecarga se llama para referencia a las funciones void func(); boost::addressof(func);. Sin embargo, eliminar la sobrecarga no impide que gcc 4.3.4 compile el código y produzca la misma salida, por lo que todavía no entiendo por qué es necesario tener esta sobrecarga.
Matthieu M.
@ Matthieu: Parece ser un error en gcc. De acuerdo con 13.3.3.2, tanto T como T * son indistinguibles para las funciones. La primera es una conversión de identidad y la segunda es la conversión de función a puntero, ambas con rango de "coincidencia exacta" (13.3.3.1.1 tabla 9). Entonces es necesario tener un argumento adicional.
Konstantin Oznobihin
@Matthieu: solo lo intenté con gcc 4.3.4 ( ideone.com/2f34P ) y obtuve la ambigüedad como se esperaba. ¿Intentó funciones miembro sobrecargadas como en la dirección de implementación o plantillas de funciones gratuitas? El último (como ideone.com/vjCRs ) dará como resultado una sobrecarga 'T *' que se elegirá debido a las reglas de deducción de argumentos de plantilla (14.8.2.1/2).
Konstantin Oznobihin
2
@ curiousguy: ¿Por qué crees que debería? He hecho referencia a partes estándar específicas de C ++ que prescriben qué debe hacer el compilador y todos los compiladores a los que tengo acceso (incluidos, entre otros, gcc 4.3.4, comeau-online, VC6.0-VC2010) informan ambigüedad tal como lo he descrito. ¿Podría por favor elaborar su razonamiento con respecto a este caso?
Konstantin Oznobihin
11

He visto una implementación de addressofhacer esto:

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

¡No me preguntes cómo se conforma esto!

Luc Danton
fuente
55
Legal. char*es la excepción enumerada para las reglas de alias de tipo.
Cachorro
66
@DeadMG No estoy diciendo que esto no sea conforme. Estoy diciendo que no deberías preguntarme :)
Luc Danton
1
@DeadMG No hay ningún problema de alias aquí. La pregunta es: está reinterpret_cast<char*>bien definido.
curioso
2
@curiousguy y la respuesta es sí, siempre está permitido emitir cualquier tipo de puntero [unsigned] char *y, por lo tanto, leer la representación de objeto del objeto señalado. Esta es otra área donde chartiene privilegios especiales.
underscore_d
@underscore_d El hecho de que un reparto esté "siempre permitido" no significa que pueda hacer nada con el resultado del reparto.
curioso
5

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

Konstantin Oznobihin
fuente
1
El código Boost, aunque interesante, no explica cómo funciona su técnica (ni explica por qué se necesitan dos sobrecargas).
James McNellis
¿Te refieres a la sobrecarga de "T * f (T * v, int) estática en línea estática"? Parece que solo era necesario para la solución alternativa de Borland C. El enfoque utilizado allí es bastante sencillo. Lo único sutil (no estándar) es la conversión de 'T &' a 'char &'. Aunque es estándar, permite la conversión de 'T *' a 'char *' parece que no existen tales requisitos para la conversión de referencia. Sin embargo, uno podría esperar que funcione exactamente igual en la mayoría de los compiladores.
Konstantin Oznobihin
@ Konstantin: la sobrecarga se usa porque para un puntero, addressofdevuelve el puntero en sí. Es discutible si es lo que el usuario quería o no, pero así es como se especifica.
Matthieu M.
@ Matthieu: ¿estás seguro? Por lo que puedo decir, cualquier tipo (incluidos los tipos de puntero) está envuelto dentro de un addr_impl_ref, por lo que la sobrecarga del puntero nunca debe llamarse ...
Matteo Italia
1
@KonstantinOznobihin esto realmente no responde a la pregunta, ya que todo lo que dice es dónde buscar la respuesta, no cuál es la respuesta .