Comment puis-je obtenir de manière fiable l'adresse d'un objet lorsque l'opérateur & est surchargé?

170

Considérez le programme suivant:

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 :'( 
}

Comment obtenir l clyde'adresse de?

Je recherche une solution qui fonctionnera aussi bien pour tous les types d'objets. Une solution C ++ 03 serait bien, mais je suis également intéressé par les solutions C ++ 11. Si possible, évitons tout comportement spécifique à l'implémentation.

Je connais le std::addressofmodèle de fonction de C ++ 11 , mais je ne suis pas intéressé à l'utiliser ici: j'aimerais comprendre comment un implémenteur de bibliothèque standard pourrait implémenter ce modèle de fonction.

James McNellis
la source
41
@jalf: Cette stratégie est acceptable, mais maintenant que j'ai frappé ces personnes dans la tête, comment contourner leur code abominable? :-)
James McNellis
5
@jalf Uhm, vous devez parfois surcharger cet opérateur et renvoyer un objet proxy. Bien que je ne puisse pas penser à un exemple pour le moment.
Konrad Rudolph le
5
@Konrad: moi non plus. Si vous en avez besoin, je suggérerais qu'une meilleure option pourrait être de repenser votre conception, car la surcharge de cet opérateur ne cause que trop de problèmes. :)
jalf
2
@Konrad: En environ 20 ans de programmation C ++, j'ai tenté une fois de surcharger cet opérateur. C'était au tout début de ces vingt ans. Oh, et je n'ai pas réussi à rendre cela utilisable. Par conséquent, l' entrée de la FAQ sur la surcharge de l' opérateur indique "L'opérateur d'adresse unaire ne doit jamais être surchargé. Vous recevrez une bière gratuite la prochaine fois que nous nous rencontrerons si vous pouvez trouver un exemple convaincant pour surcharger cet opérateur. (Je sais que vous quittez Berlin, donc je peux offrir ceci en toute sécurité :))
sbi
5
CComPtr<>et CComQIPtr<>avoir une surchargeoperator&
Simon Richter

Réponses:

102

Mise à jour: en C ++ 11, on peut utiliser à la std::addressofplace de boost::addressof.


Copions d'abord le code de Boost, moins le travail du compilateur autour des 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 );
}

Que se passe-t-il si nous transmettons une référence à la fonction ?

Remarque: addressofne peut pas être utilisé avec un pointeur sur la fonction

En C ++, si void func();est déclaré, alors funcest une référence à une fonction ne prenant aucun argument et ne renvoyant aucun résultat. Cette référence à une fonction peut être convertie de manière triviale en un pointeur vers une fonction - à partir de @Konstantin: Selon 13.3.3.2 les deux T &et T *sont indiscernables pour les fonctions. La première est une conversion d'identité et la deuxième est une conversion de fonction en pointeur, toutes deux ayant le rang "Exact Match" (13.3.3.1.1 tableau 9).

La référence au passage de fonctionaddr_impl_ref , il y a une ambiguïté dans la résolution de surcharge pour le choix de f, qui est résolue grâce à l'argument factice 0, qui est une intpremière et pourrait être promue en long(Conversion intégrale).

Ainsi, nous renvoyons simplement le pointeur.

Que se passe-t-il si nous passons un type avec un opérateur de conversion?

Si l'opérateur de conversion donne un T*alors nous avons une ambiguïté: pour f(T&,long)une promotion intégrale est requise pour le deuxième argument tandis que pour f(T*,int)l'opérateur de conversion est appelé sur le premier (grâce à @litb)

C'est à ce moment-là que addr_impl_refcommence. La norme C ++ stipule qu'une séquence de conversion peut contenir au plus une conversion définie par l'utilisateur. En encapsulant le type addr_impl_refet en forçant déjà l'utilisation d'une séquence de conversion, nous "désactivons" tout opérateur de conversion fourni avec le type.

Ainsi, la f(T&,long)surcharge est sélectionnée (et la promotion intégrale effectuée).

Que se passe-t-il pour tout autre type?

Ainsi la f(T&,long)surcharge est sélectionnée, car là le type ne correspond pas au T*paramètre.

Remarque: d'après les remarques dans le fichier concernant la compatibilité Borland, les tableaux ne se désintègrent pas en pointeurs, mais sont passés par référence.

Que se passe-t-il dans cette surcharge?

Nous voulons éviter d'appliquer operator&au type, car il peut avoir été surchargé.

Les garanties standard qui reinterpret_castpeuvent être utilisées pour ce travail (voir la réponse de @Matteo Italia: 5.2.10 / 10).

Boost ajoute quelques subtilités avec les qualificatifs constet volatilepour éviter les avertissements du compilateur (et utilisez correctement a const_castpour les supprimer).

  • Diffuser T&surchar const volatile&
  • Dénuder le constetvolatile
  • Appliquer l' &opérateur pour prendre l'adresse
  • Rejeter sur un T*

Le const/ la volatilejonglerie est un peu de la magie noire, mais elle simplifie le travail (plutôt que de fournir 4 surcharges). Notez que puisque Tn'est pas qualifié, si nous passons a ghost const&, alors T*est ghost const*, donc les qualificatifs n'ont pas vraiment été perdus.

EDIT: la surcharge du pointeur est utilisée pour le pointeur vers les fonctions, j'ai quelque peu modifié l'explication ci-dessus. Je ne comprends toujours pas pourquoi c'est nécessaire .

La sortie d'ideone suivante résume cela, un peu.

Matthieu M.
la source
2
"Que se passe-t-il si nous passons un pointeur?" une partie est incorrecte. Si nous passons un pointeur vers un type U, l'adresse de la fonction de type 'T' est déduite comme étant 'U *' et addr_impl_ref aura deux surcharges: 'f (U * &, long)' et 'f (U **, int) ', évidemment le premier sera sélectionné.
Konstantin Oznobihin le
@Konstantin: d'accord, j'avais pensé que les deux fsurcharges étaient des modèles de fonctions, alors que ce sont des fonctions membres régulières d'une classe de modèle, merci de le signaler. (Maintenant, j'ai juste besoin de comprendre à quoi sert la surcharge, un conseil?)
Matthieu M.
C'est une excellente réponse bien expliquée. J'ai en quelque sorte pensé qu'il y avait un peu plus que simplement «passer à travers char*». Merci Matthieu.
James McNellis
@James: J'ai eu beaucoup d'aide de @Konstantin qui me frappait la tête avec un bâton chaque fois que je faisais une erreur: D
Matthieu M.
3
Pourquoi aurait-il besoin de contourner les types qui ont une fonction de conversion? Ne préférerait-il pas la correspondance exacte à l'invocation d'une fonction de conversion T*? EDIT: Maintenant, je vois. Ce serait le cas, mais avec l' 0argumentation, cela aboutirait à un entrecroisement , ce serait donc ambigu.
Johannes Schaub - litb
99

Utilisez std::addressof.

Vous pouvez y penser comme faisant ce qui suit dans les coulisses:

  1. Réinterpréter l'objet comme une référence à un caractère
  2. Prenez l'adresse de ça (n'appelle pas la surcharge)
  3. Rejetez le pointeur sur un pointeur de votre type.

Les implémentations existantes (y compris Boost.Addressof) font exactement cela, en prenant simplement soin constet en volatilequalification.

Konrad Rudolph
la source
16
J'aime cette explication mieux que la sélection sur car elle peut être facilement comprise.
Sled
49

L'astuce derrière boost::addressofet l'implémentation fournie par @Luc Danton repose sur la magie du reinterpret_cast; la norme indique explicitement au §5.2.10 ¶10 que

Une expression lvalue de type T1peut être convertie en type «référence à T2» si une expression de type «pointeur vers T1» peut être explicitement convertie en type «pointeur vers T2» à l'aide de a reinterpret_cast. Autrement dit, une distribution de référence reinterpret_cast<T&>(x)a le même effet que la conversion *reinterpret_cast<T*>(&x)avec les opérateurs &et intégrés *. Le résultat est une lvalue qui fait référence au même objet que la lvalue source, mais avec un type différent.

Maintenant, cela nous permet de convertir une référence d'objet arbitraire en a char &(avec une qualification cv si la référence est qualifiée cv), car tout pointeur peut être converti en a (éventuellement qualifié cv) char *. Maintenant que nous avons un char &, la surcharge de l'opérateur sur l'objet n'est plus pertinente, et nous pouvons obtenir l'adresse avec l' &opérateur intégré .

L'implémentation boost ajoute quelques étapes pour travailler avec des objets qualifiés cv: la première reinterpret_castest faite pour const volatile char &, sinon un char &cast brut ne fonctionnerait pas pour constet / ou volatileréférences ( reinterpret_castne peut pas supprimer const). Ensuite, constet volatileest supprimé avec const_cast, l'adresse est prise avec &, et un final reinterpet_castau type "correct" est effectué.

Le const_castest nécessaire pour supprimer le const/ volatilequi aurait pu être ajouté à des références non const / volatiles, mais il ne "nuit" pas à ce qui était une référence const/ volatileen premier lieu, car le final reinterpret_castajoutera à nouveau la qualification cv si c'était là en premier lieu ( reinterpret_castne peut pas supprimer le constmais peut l'ajouter).

En ce qui concerne le reste du code addressof.hpp, il semble que la majeure partie soit pour des solutions de contournement. Le static inline T * f( T * v, int )semble être nécessaire uniquement pour le compilateur Borland, mais sa présence en introduit le besoin addr_impl_ref, sinon les types de pointeurs seraient interceptés par cette seconde surcharge.

Edit : les différentes surcharges ont une fonction différente, voir @Matthieu M. excellente réponse .

Eh bien, je n'en suis plus sûr non plus; Je devrais approfondir ce code, mais maintenant je prépare le dîner :), je vais y jeter un œil plus tard.

Matteo Italia
la source
L'explication de Matthieu M. concernant le passage du pointeur à addressof est incorrecte. Ne gâchez pas votre excellente réponse avec de telles modifications :)
Konstantin Oznobihin
"bon appétit", une enquête plus approfondie montre que la surcharge est appelée pour référence à des fonctions void func(); boost::addressof(func);. Cependant, la suppression de la surcharge n'empêche pas gcc 4.3.4 de compiler le code et de produire la même sortie, donc je ne comprends toujours pas pourquoi il est nécessaire d'avoir cette surcharge.
Matthieu M.
@Matthieu: Cela semble être un bogue dans gcc. Selon 13.3.3.2, T & et T * sont indiscernables pour les fonctions. La première est une conversion d'identité et la deuxième est une conversion de fonction en pointeur, toutes deux ayant le rang "Exact Match" (13.3.3.1.1 tableau 9). Il est donc nécessaire d'avoir un argument supplémentaire.
Konstantin Oznobihin le
@Matthieu: Je l'ai juste essayé avec gcc 4.3.4 ( ideone.com/2f34P ) et j'ai obtenu l'ambiguïté comme prévu. Avez-vous essayé des fonctions membres surchargées comme dans l'adresse d'implémentation ou des modèles de fonction gratuits? Ce dernier (comme ideone.com/vjCRs ) entraînera une surcharge 'T *' à choisir en raison des règles de déduction des arguments temlate (14.8.2.1/2).
Konstantin Oznobihin le
2
@curiousguy: Pourquoi pensez-vous que cela devrait? J'ai référencé des parties standard C ++ spécifiques prescrivant ce que doit faire le compilateur et tous les compilateurs auxquels j'ai accès (y compris, mais sans s'y limiter, gcc 4.3.4, comeau-online, VC6.0-VC2010) signalent l'ambiguïté comme je l'ai décrit. Pourriez-vous préciser votre raisonnement concernant cette affaire?
Konstantin Oznobihin
11

J'ai vu une implémentation de addressoffaire ceci:

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

Ne me demandez pas à quel point c'est conforme!

Luc Danton
la source
5
Légal. char*est l'exception répertoriée pour les règles d'alias de type.
Puppy
6
@DeadMG Je ne dis pas que ce n'est pas conforme. Je dis que tu ne devrais pas me demander :)
Luc Danton
1
@DeadMG Il n'y a pas de problème d'alias ici. La question est: est reinterpret_cast<char*>bien définie.
curiousguy
2
@curiousguy et la réponse est oui, il est toujours permis de convertir n'importe quel type de pointeur [unsigned] char *et ainsi de lire la représentation d'objet de l'objet pointé. Ceci est un autre domaine où chara des privilèges spéciaux.
underscore_d
@underscore_d Ce n'est pas parce qu'un casting est "toujours autorisé" que vous pouvez faire quoi que ce soit avec le résultat du casting.
curiousguy
5

Jetez un œil à boost :: addressof et à son implémentation.

Konstantin Oznobihin
la source
1
Le code Boost, bien qu'intéressant, n'explique pas comment sa technique fonctionne (pas plus qu'il n'explique pourquoi deux surcharges sont nécessaires).
James McNellis
voulez-vous dire surcharge «statique en ligne T * f (T * v, int)»? Il semble nécessaire pour la solution de contournement Borland C. L'approche utilisée ici est assez simple. La seule chose subtile (non standard) qui existe est la conversion de 'T &' en 'char &'. Bien que la norme permette la coulée de «T *» à «char *», il ne semble pas y avoir de telles exigences pour la coulée de référence. Néanmoins, on pourrait s'attendre à ce qu'il fonctionne exactement de la même manière sur la plupart des compilateurs.
Konstantin Oznobihin le
@Konstantin: la surcharge est utilisée car pour un pointeur, addressofrenvoie le pointeur lui-même. On peut se demander si c'est ce que l'utilisateur voulait ou non, mais c'est ainsi qu'il l'a spécifié.
Matthieu M.
@Matthieu: êtes-vous sûr? Pour autant que je sache, tout type (y compris les types de pointeur) est enveloppé dans un addr_impl_ref, donc la surcharge du pointeur ne devrait jamais être appelée ...
Matteo Italia
1
@KonstantinOznobihin cela ne répond pas vraiment à la question, car tout ce que vous dites est de savoir chercher la réponse, pas quelle est la réponse .