Come posso ottenere in modo affidabile l'indirizzo di un oggetto quando l'operatore è sovraccarico?


170

Considera il seguente programma:

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

Come ottengo clydel'indirizzo?

Sto cercando una soluzione che funzioni ugualmente bene per tutti i tipi di oggetti. Una soluzione C ++ 03 sarebbe bella, ma sono interessato anche alle soluzioni C ++ 11. Se possibile, evitiamo qualsiasi comportamento specifico dell'implementazione.

Sono a conoscenza del std::addressofmodello di funzione di C ++ 11 , ma non mi interessa utilizzarlo qui: vorrei capire come un implementatore della libreria standard potrebbe implementare questo modello di funzione.


41
@jalf: Questa strategia è accettabile, ma ora che ho dato un pugno a qualcuno in testa, come posso aggirare il suo abominevole codice? :-)
James McNellis,

5
@jalf Uhm, a volte è necessario sovraccaricare questo operatore e restituire un oggetto proxy. Anche se non riesco a pensare a un esempio proprio ora.
Konrad Rudolph,

5
@Konrad: neanche io. Se ne hai bisogno, suggerirei che un'opzione migliore potrebbe essere quella di ripensare il tuo progetto, perché sovraccaricare quell'operatore causa solo troppi problemi. :)
jalf

2
@Konrad: In circa 20 anni di programmazione in C ++ ho tentato una volta di sovraccaricare quell'operatore . Quello fu all'inizio di quei vent'anni. Oh, e non sono riuscito a renderlo utilizzabile. Di conseguenza, la voce FAQ sovraccarico dell'operatore dice "L'indirizzo unario dell'operatore non dovrebbe mai essere sovraccaricato". Otterrai una birra gratis la prossima volta che ci incontreremo se riesci a trovare un esempio convincente per sovraccaricare questo operatore. (So ​​che stai lasciando Berlino, quindi posso tranquillamente offrirlo :))
sbi

5
CComPtr<>e CComQIPtr<>avere un sovraccaricooperator&
Simon Richter,

Risposte:


102

Aggiornamento: in C ++ 11, si può usare al std::addressofposto di boost::addressof.


Copiamo prima il codice da Boost, meno che il compilatore funzioni attorno ai bit:

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 );
}

Cosa succede se passiamo un riferimento alla funzione ?

Nota: addressofnon può essere utilizzato con un puntatore per funzionare

In C ++ se void func();è dichiarato, allora funcè un riferimento a una funzione che non accetta alcun argomento e non restituisce alcun risultato. Questo riferimento a una funzione può essere banalmente convertito in un puntatore a funzione - da @Konstantin: secondo 13.3.3.2 entrambi T &e T *sono indistinguibili per le funzioni. Il primo è una conversione di identità e il secondo è una conversione da funzione a puntatore entrambi con rango "Corrispondenza esatta" (13.3.3.1.1 tabella 9).

Il riferimento alla funzione passa attraverso addr_impl_ref, c'è un'ambiguità nella risoluzione del sovraccarico per la scelta di f, che è risolta grazie all'argomento fittizio 0, che è il intprimo e potrebbe essere promosso a long(Conversione integrale).

Quindi restituiamo semplicemente il puntatore.

Cosa succede se passiamo un tipo con un operatore di conversione?

Se l'operatore di conversione produce a T*allora abbiamo un'ambiguità: per f(T&,long)una promozione integrale è necessaria per il secondo argomento mentre per f(T*,int)l'operatore di conversione viene chiamato il primo (grazie a @litb)

Ecco quando addr_impl_refentra in gioco. Lo standard C ++ impone che una sequenza di conversione possa contenere al massimo una conversione definita dall'utente. Inserendo il tipo addr_impl_refe forzando già l'uso di una sequenza di conversione, "disabilitiamo" qualsiasi operatore di conversione fornito dal tipo.

In tal modo f(T&,long)viene selezionato il sovraccarico (e viene eseguita la promozione integrale).

Cosa succede per qualsiasi altro tipo?

Quindi f(T&,long)viene selezionato il sovraccarico, perché lì il tipo non corrisponde al T*parametro.

Nota: dalle osservazioni nel file relative alla compatibilità di Borland, le matrici non si deteriorano in puntatori, ma vengono passate per riferimento.

Cosa succede in questo sovraccarico?

Vogliamo evitare di applicare operator&al tipo, poiché potrebbe essere stato sovraccarico.

Lo standard garantisce che reinterpret_castpuò essere utilizzato per questo lavoro (vedi la risposta di @Matteo Italia: 5.2.10 / 10).

Boost aggiunge alcuni aspetti positivi conste volatilequalificatori per evitare gli avvisi del compilatore (e utilizzare correttamente a const_castper rimuoverli).

  • Cast T&tochar const volatile&
  • Striscia constevolatile
  • Applicare l' &operatore per prendere l'indirizzo
  • Torna a a T*

Il const/ volatilegiocoleria è un po 'di magia nera, ma semplifica il lavoro (piuttosto che fornire 4 sovraccarichi). Si noti che poiché Tnon è qualificato, se si passa a ghost const&, allora lo T*è ghost const*, quindi le qualificazioni non sono state realmente perse.

EDIT: il sovraccarico del puntatore viene utilizzato per il puntatore alle funzioni, ho modificato in qualche modo la spiegazione sopra. Non capisco ancora perché sia necessario .

Il seguente output di ideone lo riassume in qualche modo.


2
"Cosa succede se passiamo un puntatore?" la parte non è corretta. Se passiamo un puntatore a un tipo U l'indirizzo della funzione il tipo 'T' viene dedotto come 'U *' e addr_impl_ref avrà due sovraccarichi: 'f (U * &, long)' e 'f (U **, int) ', ovviamente verrà selezionato il primo.
Konstantin Oznobihin,

@Konstantin: giusto, avevo pensato che i due fsovraccarichi fossero modelli di funzioni, mentre sono normali funzioni membro di una classe di template, grazie per averlo sottolineato. (Ora ho solo bisogno di capire qual è l'uso del sovraccarico, qualche consiglio?)
Matthieu M.

Questa è un'ottima risposta ben spiegata. Ho pensato che ci fosse un po 'di più in questo oltre al semplice "cast through char*". Grazie Matthieu.
James McNellis,

@James: ho avuto molto aiuto da @Konstantin che mi avrebbe colpito la testa ogni volta che avrei fatto un errore: D
Matthieu M.

3
Perché dovrebbe essere necessario aggirare i tipi che hanno una funzione di conversione? Non preferirebbe la corrispondenza esatta piuttosto che invocare una funzione di conversione T*? EDIT: ora vedo. Lo farebbe, ma con l' 0argomentazione finirebbe in una croce , quindi sarebbe ambiguo.
Johannes Schaub - litb

99

Usa std::addressof.

Puoi pensarlo nel modo seguente dietro le quinte:

  1. Reinterpreta l'oggetto come riferimento a carattere
  2. Prendi l'indirizzo di quello (non chiamerà il sovraccarico)
  3. Trasmetti il ​​puntatore a un puntatore del tuo tipo.

Le implementazioni esistenti (incluso Boost.Addressof) fanno esattamente questo, solo prendendosi cura conste volatilequalificazione aggiuntive .


16
Mi piace questa spiegazione meglio di quella selezionata in quanto può essere facilmente compresa.
Slitta

49

Il trucco boost::addressofe l'implementazione forniti da @Luc Danton si basano sulla magia di reinterpret_cast; lo standard afferma esplicitamente in §5.2.10 ¶10 che

È T1possibile eseguire il cast di un'espressione di tipo lvalue nel tipo “riferimento a T2” se un'espressione di tipo “puntatore a T1” può essere esplicitamente convertita nel tipo “puntatore a T2” utilizzando a reinterpret_cast. Cioè, un cast di riferimento reinterpret_cast<T&>(x)ha lo stesso effetto della conversione *reinterpret_cast<T*>(&x)con l' operatore incorporato &e gli *operatori. Il risultato è un valore che fa riferimento allo stesso oggetto del valore di origine, ma con un tipo diverso.

Ora, questo ci consente di convertire un riferimento ad un oggetto arbitrario in un char &(con una qualifica cv se il riferimento è qualificato in cv), perché qualsiasi puntatore può essere convertito in un (possibilmente qualificato in cv) char *. Ora che abbiamo un char &, il sovraccarico dell'operatore sull'oggetto non è più rilevante e possiamo ottenere l'indirizzo con l' &operatore incorporato .

L'implementazione del boost aggiunge alcuni passaggi per lavorare con oggetti qualificati per cv: il primo reinterpret_castviene fatto const volatile char &, altrimenti un char &cast normale non funzionerebbe per conste / o volatileriferimenti ( reinterpret_castnon può essere rimosso const). Quindi conste volatileviene rimosso con const_cast, l'indirizzo viene preso con &e viene eseguito un tipo finale reinterpet_cast"corretto".

Il const_castè necessario per rimuovere il const/ volatileche avrebbe potuto essere aggiunto al non-const / riferimenti volatili, ma non "danno": quello che era un const/ volatiledi riferimento in primo luogo, perché la finale reinterpret_castsi ri-aggiungere il cv-qualificazione se fosse lì in primo luogo ( reinterpret_castnon è possibile rimuovere il constma può aggiungerlo).

Per quanto riguarda il resto del codice addressof.hpp, sembra che la maggior parte sia per soluzioni alternative. L' static inline T * f( T * v, int )sembra essere necessario solo per il compilatore Borland, ma la sua presenza introduce la necessità di addr_impl_ref, altrimenti tipi di puntatore sarebbe stato catturato da questa seconda sovraccarico.

Modifica : i vari sovraccarichi hanno una funzione diversa, vedi @Matthieu M. risposta eccellente .

Bene, non ne sono più sicuro neanche; Dovrei indagare ulteriormente su quel codice, ma ora sto cucinando la cena :), ci darò un'occhiata più tardi.


La spiegazione di Matthieu M. riguardo al passaggio del puntatore a addressof non è corretta. Non rovinare la tua grande risposta con tali modifiche :)
Konstantin Oznobihin,

"buon appetito", ulteriori indagini dimostrano che il sovraccarico è chiamato come riferimento alle funzioni void func(); boost::addressof(func);. Tuttavia, rimuovere il sovraccarico non impedisce a gcc 4.3.4 di compilare il codice e produrre lo stesso output, quindi non capisco ancora perché sia necessario avere questo sovraccarico.
Matthieu M.,

@Matthieu: sembra essere un bug in gcc. Secondo 13.3.3.2 sia T & sia T * non sono distinguibili per le funzioni. Il primo è una conversione di identità e il secondo è una conversione da funzione a puntatore entrambi con rango "Corrispondenza esatta" (13.3.3.1.1 tabella 9). Quindi è necessario avere ulteriori argomenti.
Konstantin Oznobihin,

@Matthieu: ho appena provato con gcc 4.3.4 ( ideone.com/2f34P ) e ho ottenuto l'ambiguità come previsto. Hai provato funzioni membro sovraccaricate come nell'implementazione dell'indirizzo o modelli di funzioni gratuiti? Quest'ultimo (come ideone.com/vjCRs ) comporterà la scelta del sovraccarico 'T *' a causa delle regole di deduzione dell'argomento temlate (14.8.2.1/2).
Konstantin Oznobihin,

2
@curiousguy: Perché pensi che dovrebbe? Ho fatto riferimento a specifiche parti standard C ++ che prescrivono cosa dovrebbe fare il compilatore e tutti i compilatori a cui ho accesso (incluso ma non limitato a gcc 4.3.4, comeau-online, VC6.0-VC2010) riportano l'ambiguità proprio come ho descritto. Potresti per favore elaborare il tuo ragionamento riguardo a questo caso?
Konstantin Oznobihin,

11

Ho visto un'implementazione di addressoffare questo:

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

Non chiedermi quanto sia conforme!


5
Legale. char*è l'eccezione elencata per digitare le regole di aliasing.
Puppy,

6
@DeadMG Non sto dicendo che questo non è conforme. Sto dicendo che non dovresti chiedermelo :)
Luc Danton,

1
@DeadMG Non ci sono problemi di aliasing qui. La domanda è: è reinterpret_cast<char*>ben definita.
curiousguy,

2
@curiousguy e la risposta è sì, è sempre permesso lanciare qualsiasi tipo di puntatore [unsigned] char *e quindi leggere la rappresentazione dell'oggetto dell'oggetto puntato. Questa è un'altra area in cui charha privilegi speciali.
underscore_d

@underscore_d Solo perché un cast è "sempre permesso" non significa che puoi fare qualsiasi cosa con il risultato del cast.
curiousguy,

5

Dai un'occhiata a boost :: addressof e alla sua implementazione.


1
Il codice Boost, sebbene interessante, non spiega come funziona la sua tecnica (né spiega perché sono necessari due sovraccarichi).
James McNellis,

intendi sovraccarico 'statico inline T * f (T * v, int)'? Sembra necessario solo per la soluzione alternativa a Borland C. L'approccio utilizzato è piuttosto semplice. L'unica cosa sottile (non standard) è la conversione di 'T &' in 'char &'. Sebbene standard, consente il cast da 'T *' a 'char *' non sembrano esserci tali requisiti per il casting di riferimento. Tuttavia, ci si potrebbe aspettare che funzioni esattamente allo stesso modo sulla maggior parte dei compilatori.
Konstantin Oznobihin,

@Konstantin: il sovraccarico viene utilizzato perché per un puntatore, addressofrestituisce il puntatore stesso. È discutibile se è ciò che l'utente voleva o no, ma è come lo ha specificato.
Matthieu M.,

@Matthieu: sei sicuro? Per quanto ne so, qualsiasi tipo (compresi i tipi di puntatore) è racchiuso all'interno di un addr_impl_ref, quindi il sovraccarico del puntatore non dovrebbe mai essere chiamato ...
Matteo Italia,

1
@KonstantinOznobihin questo non risponde davvero alla domanda, poiché tutto ciò che dici è dove cercare la risposta, non qual è la risposta .
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.