Jak niezawodnie uzyskać adres obiektu, gdy operator & jest przeciążony?

170

Rozważ następujący program:

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

Jak uzyskać clydeadres?

Szukam rozwiązania, które sprawdzi się równie dobrze na wszystkich typach obiektów. Rozwiązanie C ++ 03 byłoby fajne, ale interesują mnie też rozwiązania C ++ 11. Jeśli to możliwe, unikajmy wszelkich zachowań specyficznych dla implementacji.

Znam std::addressofszablon funkcji C ++ 11 , ale nie jestem zainteresowany użyciem go tutaj: chciałbym zrozumieć, w jaki sposób osoba wdrażająca bibliotekę standardową może zaimplementować ten szablon funkcji.

James McNellis
źródło
41
@jalf: Ta strategia jest do przyjęcia, ale teraz, kiedy uderzyłem w głowę te osoby, jak mam obejść ich ohydny kod? :-)
James McNellis
5
@jalf Uhm, czasami trzeba przeciążyć ten operator i zwrócić obiekt proxy. Chociaż nie mogę teraz wymyślić przykładu.
Konrad Rudolph
5
@Konrad: ja też. Jeśli tego potrzebujesz, sugerowałbym, że lepszą opcją może być ponowne przemyślenie projektu, ponieważ przeciążenie tego operatora powoduje po prostu zbyt wiele problemów. :)
jalf
2
@Konrad: Przez około 20 lat programowania w C ++ próbowałem kiedyś przeciążyć ten operator. To było na samym początku tych dwudziestu lat. Aha, i nie udało mi się uczynić tego użytecznym. W konsekwencji, operator przeciążający wpis FAQ mówi: „Jednoargumentowy operator address-of nigdy nie powinien być przeciążany”. Przy następnym spotkaniu dostaniesz darmowe piwo, jeśli znajdziesz przekonujący przykład przeciążenia tego operatora. (Wiem, że wyjeżdżasz z Berlina, więc mogę to bezpiecznie zaoferować :))
sbi
5
CComPtr<>i CComQIPtr<>przeładowanyoperator&
Simon Richter

Odpowiedzi:

102

Aktualizacja: w C ++ 11 std::addressofzamiast boost::addressof.


Najpierw skopiujmy kod z Boost, bez kompilatora obejść bity:

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

Co się stanie, jeśli przekażemy odniesienie do funkcji ?

Uwaga: addressofnie można używać ze wskaźnikiem do funkcji

W C ++ if void func();jest zadeklarowane, to funcjest odwołaniem do funkcji nie pobierającej argumentu i nie zwracającej wyniku. To odniesienie do funkcji można w trywialny sposób przekształcić we wskaźnik do funkcji - z @Konstantin: Zgodnie z 13.3.3.2 zarówno T &i T *są nierozróżnialne dla funkcji. Pierwsza z nich to konwersja tożsamości, a druga to konwersja funkcji do wskaźnika, obie mają rangę „Dokładne dopasowanie” (13.3.3.1.1 tabela 9).

Odniesienie do funkcji przejść addr_impl_refnie jest niejednoznaczność rozdzielczości przeciążeniem wyboru f, który jest rozwiązany dzięki argumentu zastępczą 0, która jest intpierwszym i może być podniesione na long(całka konwersji).

W ten sposób po prostu zwracamy wskaźnik.

Co się stanie, jeśli przekażemy typ z operatorem konwersji?

Jeśli operator konwersji daje a, T*to mamy niejednoznaczność: ponieważ f(T&,long)Integral Promotion jest wymagana dla drugiego argumentu, a dla f(T*,int)konwersji operator jest wywoływany na pierwszym (dzięki @litb)

Wtedy addr_impl_refzaczyna się. Standard C ++ nakazuje, aby sekwencja konwersji zawierała co najwyżej jedną konwersję zdefiniowaną przez użytkownika. Zawijając typ addr_impl_refi wymuszając już użycie sekwencji konwersji, „wyłączamy” każdy operator konwersji, z którym ten typ pochodzi.

W ten sposób f(T&,long)wybierane jest przeciążenie (i wykonywana jest promocja integralna).

Co dzieje się z każdym innym typem?

W ten sposób f(T&,long)wybrane jest przeciążenie, ponieważ tam typ nie pasuje do T*parametru.

Uwaga: z uwag w pliku dotyczących kompatybilności Borlanda, tablice nie rozpadają się na wskaźniki, ale są przekazywane przez odniesienie.

Co się dzieje w tym przeciążeniu?

Chcemy uniknąć stosowania operator&do typu, ponieważ mógł być przeciążony.

Standardowe gwarancje, które reinterpret_castmożna wykorzystać w tej pracy (patrz odpowiedź @Matteo Italia: 5.2.10 / 10).

Boost dodaje kilka dodatków z kwalifikatorami consti, volatileaby uniknąć ostrzeżeń kompilatora (i poprawnie użyj a, const_castaby je usunąć).

  • Przesyłaj T&dochar const volatile&
  • Usuń constivolatile
  • Zastosuj &operatora, aby przejąć adres
  • Odrzuć z powrotem do T*

const/ volatileŻonglerka jest nieco czarnej magii, ale ma uprościć pracę (raczej niż dostarczanie 4 przeciążeń). Zauważ, że ponieważ Tjest niekwalifikowany, jeśli zdamy a ghost const&, to T*jest ghost const*, więc kwalifikatory tak naprawdę nie zostały utracone.

EDYCJA: przeciążenie wskaźnika służy do wskazywania wskaźników do funkcji, nieco poprawiłem powyższe wyjaśnienie. Nadal nie rozumiem, dlaczego jest to konieczne .

Poniższe wyniki ideone nieco to podsumowują.

Matthieu M.
źródło
2
„Co się stanie, jeśli miniemy wskaźnik?” część jest nieprawidłowa. Jeśli przekażemy wskaźnik do jakiegoś typu U, adres funkcji, typ 'T' jest wywnioskowany jako 'U *', a addr_impl_ref będzie miał dwa przeciążenia: 'f (U * &, long)' i 'f (U **, int) ', oczywiście zostanie wybrany pierwszy.
Konstantin Oznobihin
@Konstantin: tak, myślałem, że dwa fprzeciążenia są szablonami funkcji, podczas gdy są one zwykłymi funkcjami składowymi klasy szablonu, dziękuję za wskazanie. (Teraz muszę tylko dowiedzieć się, do czego służy przeciążenie, jakaś wskazówka?)
Matthieu M.
To świetna, dobrze wyjaśniona odpowiedź. Doszedłem do wniosku, że jest w tym coś więcej niż tylko „przerzucenie char*”. Dziękuję Matthieu.
James McNellis
@James: Dużo pomógł mi @Konstantin, który uderzył mnie w głowę kijem za każdym razem, gdy popełniłem błąd: D
Matthieu M.
3
Dlaczego miałoby obejść typy, które mają funkcję konwersji? Czy nie wolałby dokładnego dopasowania niż wywoływania funkcji konwersji T*? EDYCJA: Teraz widzę. Mógłby, ale z 0argumentem skończyłby się na krzyż , więc byłoby niejednoznaczne.
Johannes Schaub - litb
99

Użyj std::addressof.

Możesz myśleć o tym jako o wykonywaniu następujących czynności za kulisami:

  1. Zinterpretuj ponownie obiekt jako odniesienie do znaku
  2. Weź adres tego (nie wywoła przeciążenia)
  3. Przerzuć wskaźnik z powrotem na wskaźnik swojego typu.

Istniejące implementacje (w tym Boost.Addressof) zrobić dokładnie to, po prostu biorąc dodatkową opiekę consti volatilekwalifikacji.

Konrad Rudolph
źródło
16
To wyjaśnienie podoba mi się bardziej niż wybrane, ponieważ można je łatwo zrozumieć.
Sanki
49

Sztuczka stojąca za boost::addressofi implementacja dostarczona przez @Luc Danton opiera się na magii reinterpret_cast; standard wyraźnie stwierdza w §5.2.10, ak.10

Wyrażenie l-wartości typu T1można rzutować na typ „odwołanie do T2”, jeśli wyrażenie typu „wskaźnik do T1” można jawnie przekonwertować na typ „wskaźnik do T2” przy użyciu rozszerzenia reinterpret_cast. Oznacza to, że rzutowanie referencyjne reinterpret_cast<T&>(x)ma taki sam efekt, jak konwersja *reinterpret_cast<T*>(&x)przy użyciu operatorów wbudowanych &i *. Wynikiem jest lwartość, która odnosi się do tego samego obiektu co źródłowa lwartość, ale z innym typem.

Teraz pozwala nam to przekonwertować dowolne odwołanie do obiektu na a char &(z kwalifikacją cv, jeśli referencja jest kwalifikowana jako cv), ponieważ dowolny wskaźnik można przekonwertować na (prawdopodobnie kwalifikowany cv) char *. Teraz, gdy mamy a char &, przeciążenie operatora na obiekcie nie jest już istotne i możemy uzyskać adres za pomocą &operatora wbudowanego .

Implementacja przyspieszenia dodaje kilka kroków do pracy z obiektami kwalifikowanymi w cv: pierwszy reinterpret_castjest wykonywany const volatile char &, w przeciwnym razie zwykłe char &rzutowanie nie działałoby dla consti / lub volatilereferencji ( reinterpret_castnie można ich usunąć const). Następnie usuwane jest consti za volatilepomocą const_cast, adres jest brany za pomocą &, a końcowe reinterpet_castdo „prawidłowego” typu jest wykonywane.

const_castJest potrzebne do usunięcia const/ volatile, które mogły zostać dodane do const / Referencje lotnych, ale nie „szkodzić”, co to jest const/ volatileodniesienia w pierwszej kolejności, ponieważ ostateczna reinterpret_castponownie dodać CV kwalifikacje, czy to tam na pierwszym miejscu ( reinterpret_castnie można usunąć, constale można go dodać).

Co do reszty kodu w programie addressof.hpp, wydaje się, że większość z niego dotyczy obejść. static inline T * f( T * v, int )Wydaje się być potrzebna tylko dla kompilatora Borland, ale jego obecność wprowadza konieczność addr_impl_ref, inaczej typy wskaźnik byłby złapany przez tego drugiego przeciążenia.

Edycja : różne przeciążenia mają inną funkcję, patrz @Matthieu M. doskonała odpowiedź .

Cóż, tego też nie jestem już pewien; Powinienem dokładniej zbadać ten kod, ale teraz gotuję obiad :), przyjrzę się temu później.

Matteo Italia
źródło
Wyjaśnienie Matthieu M. dotyczące przekazywania wskaźnika do addressof jest nieprawidłowe. Nie psuj swojej świetnej odpowiedzi takimi edycjami :)
Konstantin Oznobihin
„dobry apetyt”, dalsze badania pokazują, że przeciążenie jest wywoływane w odniesieniu do funkcji void func(); boost::addressof(func);. Jednak usunięcie przeciążenia nie zapobiega kompilowaniu kodu przez gcc 4.3.4 i generowaniu tych samych danych wyjściowych, więc nadal nie rozumiem, dlaczego konieczne jest posiadanie tego przeciążenia.
Matthieu M.
@Matthieu: Wygląda na błąd w gcc. Zgodnie z 13.3.3.2 zarówno T &, jak i T * są nierozróżnialne dla funkcji. Pierwsza z nich to konwersja tożsamości, a druga to konwersja funkcji do wskaźnika, obie mają rangę „Dokładne dopasowanie” (13.3.3.1.1 tabela 9). Trzeba więc mieć dodatkowy argument.
Konstantin Oznobihin
@Matthieu: Właśnie wypróbowałem to z gcc 4.3.4 ( ideone.com/2f34P ) i otrzymałem niejednoznaczność zgodnie z oczekiwaniami. Czy próbowałeś przeciążonych funkcji członkowskich, takich jak implementacja adresu lub bezpłatne szablony funkcji? Ta ostatnia (jak ideone.com/vjCRs ) spowoduje wybranie przeciążenia „T *” ze względu na reguły dedukcji argumentów temlate (14.8.2.1/2).
Konstantin Oznobihin
2
@curiousguy: Jak myślisz, dlaczego tak powinno? Odwołałem się do określonych standardowych części C ++, które określają, co powinien zrobić kompilator, a wszystkie kompilatory, do których mam dostęp (w tym między innymi gcc 4.3.4, comeau-online, VC6.0-VC2010) zgłaszają niejednoznaczność, tak jak opisałem. Czy mógłbyś wyjaśnić swoje rozumowanie w tej sprawie?
Konstantin Oznobihin
11

Widziałem implementację addressofzrób to:

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

Nie pytajcie mnie, jak to jest zgodne!

Luc Danton
źródło
5
Prawny. char*jest wymienionym wyjątkiem od reguł aliasingu.
Puppy
6
@DeadMG Nie mówię, że to nie jest zgodne. Mówię, że nie powinieneś mnie o to pytać :)
Luc Danton
1
@DeadMG Nie ma tutaj problemu z aliasowaniem. Pytanie brzmi: jest reinterpret_cast<char*>dobrze zdefiniowane.
curiousguy
2
@curiousguy, a odpowiedź brzmi tak, zawsze można rzutować dowolny typ wskaźnika, [unsigned] char *a tym samym odczytywać reprezentację obiektu wskazywanego obiektu. To kolejny obszar, który charma specjalne przywileje.
underscore_d
@underscore_d Tylko dlatego, że rzutowanie jest „zawsze dozwolone”, nie oznacza, że ​​możesz zrobić cokolwiek z wynikiem rzutowania.
curiousguy
5

Przyjrzyj się boost :: addressof i jego implementacji.

Konstantin Oznobihin
źródło
1
Kod Boost, choć interesujący, nie wyjaśnia, jak działa jego technika (ani nie wyjaśnia, dlaczego potrzebne są dwa przeciążenia).
James McNellis
czy masz na myśli przeciążenie „static inline T * f (T * v, int)”? Wygląda na to, że jest potrzebny tylko do obejścia Borland C. Zastosowane podejście jest dość proste. Jedyną subtelną (niestandardową) rzeczą jest konwersja „T &” na „char &”. Chociaż standardowe, pozwala na rzutowanie z „T *” na „char *”, wydaje się, że nie ma takich wymagań dla rzutowania referencyjnego. Niemniej jednak można by oczekiwać, że będzie działał dokładnie tak samo na większości kompilatorów.
Konstantin Oznobihin
@Konstantin: przeciążenie jest używane, ponieważ dla wskaźnika addressofzwraca sam wskaźnik. Można się spierać, czy tego chciał użytkownik, czy nie, ale tak to określił.
Matthieu M.
@Matthieu: czy na pewno? O ile wiem, każdy typ (w tym typy wskaźników) jest zawijany wewnątrz an addr_impl_ref, więc przeciążenie wskaźnika nigdy nie powinno być nazywane ...
Matteo Italia
1
@KonstantinOznobihin to tak naprawdę nie odpowiada na pytanie, ponieważ mówisz tylko, gdzie szukać odpowiedzi, a nie jaka jest odpowiedź .