Как я могу надежно получить адрес объекта, когда оператор & перегружен?

Рассмотрим следующую программу:

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

Как мне получить адрес clyde?

Я ищу решение, которое будет одинаково хорошо работать для всех типов объектов. Решение С++ 03 было бы неплохо, но меня тоже интересуют решения на С++ 11. Если возможно, избегайте любого поведения, специфичного для конкретной реализации.

Мне известно о шаблоне функций С++ 11 std::addressof, но я не заинтересован в его использовании здесь: я хотел бы понять, как разработчик Standard Library может реализовать этот шаблон функции.

Ответ 1

Обновление: в С++ 11, вместо boost::addressof можно использовать std::addressof.


Скопируем сначала код из Boost, за исключением того, что компилятор работает с битами:

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

Что произойдет, если мы передадим ссылку на функцию?

Примечание: addressof не может использоваться с указателем на функцию

В С++, если объявлено void func();, тогда func является ссылкой на функцию, не принимающую никаких аргументов и не возвращающую результат. Эта ссылка на функцию может быть тривиально преобразована в указатель на функцию - от @Konstantin: согласно 13.3.3.2 оба T & и T * неотличимы для функций. Первый - это преобразование Идентичности, а второе - преобразование из функции в указатель, имеющее ранг "Точное совпадение" (таблица 13.3.3.1.1, таблица 9).

Ссылка на функцию проходит через addr_impl_ref, существует двусмысленность в разрешении перегрузки для выбора f, которое решается благодаря фиктивному аргументу 0, который является int первым и может повышается до long (интегральное преобразование).

Таким образом, мы просто возвращаем указатель.

Что произойдет, если мы передадим тип с оператором преобразования?

Если оператор преобразования дает T*, тогда мы имеем двусмысленность: для f(T&,long) для второго аргумента требуется интегральное продвижение, а для f(T*,int) оператор преобразования вызывается на первом (благодаря @litb)

Это, когда addr_impl_ref запускается. Стандарт С++ требует, чтобы последовательность преобразования могла содержать не более одного определяемого пользователем преобразования. Упаковывая тип в addr_impl_ref и заставляя использовать последовательность преобразования уже, мы "отключим" любой оператор преобразования, к которому относится тип.

Таким образом, выбирается перегрузка f(T&,long) (и выполняется интегральное продвижение).

Что происходит для любого другого типа?

Таким образом, выбрана перегрузка f(T&,long), потому что тип не соответствует параметру T*.

Примечание: из замечаний в файле, касающихся совместимости Borland, массивы не распадаются на указатели, а передаются по ссылке.

Что происходит в этой перегрузке?

Мы хотим избежать применения operator& к типу, поскольку он, возможно, был перегружен.

Стандарт гарантирует, что reinterpret_cast может использоваться для этой работы (см. ответ @Matteo Italia: 5.2.10/10).

Boost добавляет некоторые тонкости с квалификаторами const и volatile, чтобы избежать предупреждений компилятора (и правильно использовать const_cast для их удаления).

  • Вставить T& в char const volatile&
  • Разделите const и volatile
  • Примените оператор &, чтобы принять адрес
  • Вернитесь к T*

Жонглирование const/volatile немного черной магии, но оно упрощает работу (а не обеспечивает 4 перегрузки). Заметим, что поскольку T является неквалифицированным, если мы передаем a ghost const&, то T* является ghost const*, поэтому квалификаторы действительно не потеряны.

EDIT: перегрузка указателя используется для указателя на функции, я немного изменил приведенное выше описание. Я все еще не понимаю, зачем это нужно.

Следующий ideone output немного подводит итог.

Ответ 2

По существу, вы можете переосмыслить объект как ссылку на char, принять его адрес (не называть перегрузку) и вернуть указатель указателю на свой тип.

Код Boost.AddressOf делает именно это, просто заботясь о квалификации volatile и const.

Ответ 3

Трюк boost::addressof, а реализация, предоставляемая @Luc Danton, основана на магии reinterpret_cast; стандарт прямо указывает на § 5.2.10 ¶10, что

Выражение lvalue типа T1 может быть передано типу "ссылка на T2", если выражение типа "указатель на T1" может быть явно преобразовано в тип "указатель на T2" используя reinterpret_cast. То есть ссылочный листинг reinterpret_cast<T&>(x) имеет тот же эффект, что и преобразование *reinterpret_cast<T*>(&x) со встроенными операторами & и *. Результатом является lvalue, относящийся к тому же объекту, что и исходное lvalue, но с другим типом.

Теперь это позволяет нам преобразовать произвольную ссылку на объект в char & (с квалификацией cv, если ссылка имеет cv-квалификацию), потому что любой указатель может быть преобразован в (возможно, cv-qualified) char *, Теперь, когда мы имеем char &, перегрузка оператора на объект больше не актуальна, и мы можем получить адрес с помощью встроенного оператора &.

Функция boost добавляет несколько шагов для работы с объектами cv-grade: первый reinterpret_cast выполняется с const volatile char &, иначе простой char & не работает для const и/или volatile Ссылки (reinterpret_cast не могут удалить const). Затем const и volatile удаляются с помощью const_cast, адрес берется с помощью &, и завершается окончательный reinterpet_cast к "правильному" типу.

const_cast необходимо удалить const/volatile, который мог быть добавлен к неконстантным/нестабильным ссылкам, но он не "вредит" тому, что было ссылкой const/volatile в первую очередь, потому что окончательный reinterpret_cast будет повторно добавлять cv-квалификацию, если он был там на первом месте (reinterpret_cast не может удалить const, но может добавить его).

Что касается остальной части кода в addressof.hpp, кажется, что большая часть его предназначена для обходных решений. static inline T * f( T * v, int ), по-видимому, необходим только для компилятора Borland, но его присутствие указывает на необходимость addr_impl_ref, иначе типы указателей будут пойманы этой второй перегрузкой.

<ы > Изменить: различные перегрузки имеют другую функцию, см. @Matthieu M. отличный ответ.

Ну, я тоже не уверен в этом; Я должен продолжить расследование этого кода, но теперь я готовлю ужин:), я посмотрю на него позже.

Ответ 4

Я видел реализацию addressof:

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

Не спрашивайте меня, насколько это соответствует!

Ответ 5

Взгляните на boost:: addressof и его реализацию.