web-dev-qa-db-fra.com

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

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'adresse de clyde?

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 modèle de fonction std::addressof 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.

167
James McNellis

Mise à jour: en C++ 11, on peut utiliser std::addressof Au lieu 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 passons une référence à la fonction ?

Remarque: addressof ne peut pas être utilisé avec un pointeur pour fonctionner

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

La référence à la fonction passe par addr_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 d'abord un int et pourrait être promu en long (conversion intégrale).

Ainsi, nous renvoyons simplement le pointeur.

Que se passe-t-il si nous transmettons un type à 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é le premier (merci à @litb)

C'est alors que addr_impl_ref Entre en jeu. La norme C++ exige qu'une séquence de conversion puisse contenir au plus une conversion définie par l'utilisateur. En encapsulant le type dans addr_impl_ref Et 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 surcharge f(T&,long) est sélectionnée (et la promotion intégrale effectuée).

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

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

Remarque: d'après les remarques du fichier concernant la compatibilité avec Borland, les tableaux ne se désintègrent pas aux pointeurs, mais sont transmis 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é.

La norme garantit que reinterpret_cast Peut être utilisé pour ce travail (voir la réponse de @Matteo Italia: 5.2.10/10).

Boost ajoute quelques subtilités avec les qualificatifs const et volatile pour éviter les avertissements du compilateur (et utilisez correctement un const_cast Pour les supprimer).

  • Cast T& Sur char const volatile&
  • Supprimez les const et volatile
  • Appliquer l'opérateur & Pour prendre l'adresse
  • Restituez un T*

Le jonglage const/volatile est un peu de magie noire, mais il simplifie le travail (plutôt que de fournir 4 surcharges). Notez que puisque T n'est pas qualifié, si nous passons un 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 sur les fonctions, j'ai quelque peu modifié l'explication ci-dessus. Je ne comprends toujours pas pourquoi c'est nécessaire cependant.

Ce qui suit sortie idéone résume cela un peu.

98
Matthieu M.

Essentiellement, vous pouvez réinterpréter l'objet en tant que référence à char, prendre son adresse (n'appellera pas la surcharge) et reconvertir le pointeur en un pointeur de votre type.

Le code Boost.AddressOf fait exactement cela, en prenant juste soin de la qualification volatile et const.

97
Konrad Rudolph

L'astuce derrière boost::addressof et 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 de valeur l de type T1 Peut ê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 d'un reinterpret_cast. Autrement dit, une conversion 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 un char & (Avec une qualification cv si la référence est qualifiée cv), car tout pointeur peut être converti en (é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 de boost ajoute quelques étapes pour travailler avec des objets qualifiés par cv: le premier reinterpret_cast Est fait pour const volatile char &, Sinon une distribution simple de char & Ne fonctionnerait pas pour const et/ou volatile références (reinterpret_cast ne peut pas supprimer const). Ensuite, le const et le volatile sont supprimés avec const_cast, L'adresse est prise avec &, Et un dernier reinterpet_cast Au "correct" "le type est terminé.

Le const_cast Est nécessaire pour supprimer le const/volatile qui aurait pu être ajouté à des références non const/volatiles, mais cela ne "nuit" pas à ce qui était un const/volatile référence en premier lieu, car le reinterpret_cast final ajoutera de nouveau la qualification cv si elle était là en premier lieu (reinterpret_cast ne peut pas supprimer le const mais peut l'ajouter).

En ce qui concerne le reste du code dans addressof.hpp, Il semble que la plupart soit destiné à des solutions de contournement. La static inline T * f( T * v, int ) semble être nécessaire uniquement pour le compilateur Borland, mais sa présence introduit le besoin de addr_impl_ref, Sinon les types de pointeurs seraient interceptés par cette deuxième 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.

49
Matteo Italia

J'ai vu une implémentation de addressof faire ceci:

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

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

11
Luc Danton

Jetez un œil à boost :: addressof et à sa mise en œuvre.

5