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

170

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

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

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

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

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

Джеймс МакНеллис
источник
41
@jalf: Эта стратегия приемлема, но теперь, когда я ударил указанных людей по голове, как мне обойти их отвратительный код? :-)
Джеймс МакНеллис
5
@jalf Хм, иногда вам нужно перегрузить этот оператор и вернуть прокси-объект. Хотя сейчас я не могу вспомнить пример.
Конрад Рудольф
5
@ Конрад: я тоже. Если вам это нужно, я бы посоветовал лучше переосмыслить ваш дизайн, потому что перегрузка этого оператора просто вызывает слишком много проблем. :)
jalf
2
@Konrad: Примерно за 20 лет программирования на C ++ я однажды попытался перегрузить этот оператор. Это было в самом начале этих двадцати лет. О, и я не смог сделать это пригодным для использования. Следовательно, запись FAQ по перегрузке оператора гласит: «Унарный адрес оператора никогда не должен быть перегружен». В следующий раз, когда мы встретимся, вы получите бесплатное пиво, если сможете привести убедительный пример перегрузки этого оператора. (Я знаю, что ты уезжаешь из Берлина, поэтому я могу смело предложить это :))
sbi
5
CComPtr<>и CComQIPtr<>перегруженыoperator&
Саймон Рихтер

Ответы:

102

Обновление: в C ++ 11 можно использовать std::addressofвместо boost::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нельзя использовать с указателем на функцию

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

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

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

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

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

Именно тогда и addr_impl_refначинается. Стандарт C ++ требует, чтобы последовательность преобразования могла содержать не более одного пользовательского преобразования. Оборачивая тип addr_impl_refи заставляя уже использовать последовательность преобразования, мы «отключаем» любой оператор преобразования, с которым поставляется тип.

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

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

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

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

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

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

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

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

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

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

РЕДАКТИРОВАТЬ: перегрузка указателя используется для указателя на функции, я несколько исправил выше объяснение. Я до сих пор не понимаю, почему это необходимо, хотя.

Следующий вывод идеона несколько суммирует это.

Матье М.
источник
2
"Что произойдет, если мы передадим указатель?" часть неверна. Если мы передадим указатель на некоторый тип U, то в адрес функции типа T будет выведено «U *», и addr_impl_ref будет иметь две перегрузки: «f (U * &, long)» и «f (U **, int) ', очевидно, будет выбран первый.
Константин Ознобихин
@Konstantin: верно, я думал, что эти два fперегружают шаблоны функций, тогда как они являются обычными функциями-членами класса шаблонов, спасибо за указание на это. (Теперь мне просто нужно выяснить, в чем польза от перегрузки, какой-нибудь совет?)
Матье М.
Это отличный, хорошо объясненный ответ. Я подумал, что в этом есть что-то большее, чем просто «пролистать char*». Спасибо, Матье.
Джеймс МакНеллис
@James: У меня была большая помощь от @Konstantin, который ударял меня по голове палкой в ​​любое время, когда я допустил ошибку: D
Matthieu M.
3
Зачем ему нужно работать с типами, которые имеют функцию преобразования? Разве он не предпочел бы точное совпадение по сравнению с вызовом какой-либо функции преобразования T*? РЕДАКТИРОВАТЬ: Теперь я вижу. Это было бы, но с 0аргументом это закончилось бы в перекрещивании , поэтому было бы неоднозначно.
Йоханнес Шауб - лит
99

Использование std::addressof.

Вы можете думать об этом как о следующих действиях:

  1. Переосмыслить объект как ссылку на символ
  2. Возьмите адрес этого (не вызовет перегрузку)
  3. Приведите указатель обратно к указателю вашего типа.

Существующие реализации ( в том числе Boost.Addressof) делать то , что, только принимая дополнительный уход constи volatileквалификацию.

Конрад Рудольф
источник
16
Мне нравится это объяснение лучше, чем выбранное, поскольку оно может быть легко понято.
Сани
49

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

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

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

Реализация boost добавляет несколько шагов для работы с cv-квалифицированными объектами: первый 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будет повторно добавить резюме-квалификацию , если это было там на первом месте ( reinterpret_castне может удалить, constно может добавить его).

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

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

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

Matteo Italia
источник
Матье М. объяснение относительно передачи указателя на адрес неверно. Не портите свой великолепный ответ такими правками :)
Константин Ознобихин
«хороший аппетит», дальнейшие исследования показывают, что перегрузка вызывается для ссылки на функции void func(); boost::addressof(func);. Однако удаление перегрузки не мешает gcc 4.3.4 скомпилировать код и выдать тот же вывод, поэтому я до сих пор не понимаю, почему необходима эта перегрузка.
Матье М.
@Matthieu: похоже, это ошибка в gcc. Согласно 13.3.3.2 и T &, и T * неразличимы для функций. Первым является преобразование Identity, а вторым является преобразование функции в указатель, оба из которых имеют ранг «точное совпадение» (13.3.3.1.1 таблица 9). Поэтому необходимо иметь дополнительный аргумент.
Константин Ознобихин
@Matthieu: Просто попробовал это с gcc 4.3.4 ( ideone.com/2f34P ) и получил двусмысленность, как и ожидалось. Вы пробовали перегруженные функции-члены, такие как адрес реализации или бесплатные шаблоны функций? Последнее (например, ideone.com/vjCRs ) приведет к перегрузке 'T *', которая будет выбрана из-за правил вывода аргументов с помощью шаблонов (14.8.2.1/2).
Константин Ознобихин
2
@curiousguy: Почему вы думаете, что это должно? Я ссылался на конкретные стандартные части C ++, предписывающие, что должен делать компилятор, и все компиляторы, к которым у меня есть доступ (включая, но не ограничиваясь, gcc 4.3.4, comeau-online, VC6.0-VC2010), сообщают о неоднозначности отчета, как я описал. Не могли бы вы изложить свои соображения по этому делу?
Константин Ознобихин
11

Я видел реализацию addressofсделать это:

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

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

Люк Дантон
источник
5
Юридические. char*перечисленное исключение для правил наложения типов.
щенок
6
@DeadMG Я не говорю, что это не соответствует. Я говорю, что вы не должны спрашивать меня :)
Люк Дантон
1
@DeadMG Здесь нет проблем с алиасами. Вопрос: reinterpret_cast<char*>хорошо определен.
любопытный парень
2
@curiousguy, и ответ - да, всегда разрешено приводить любой тип указателя [unsigned] char *и, таким образом, читать объектное представление указанного объекта. Это еще одна область, где charесть особые привилегии.
underscore_d
@underscore_d Тот факт, что приведение «всегда разрешено», не означает, что вы можете что-либо сделать с результатом приведения.
любопытный парень
5

Посмотрите на boost :: addressof и его реализацию.

Константин Ознобихин
источник
1
Код Boost, хотя и интересен, не объясняет, как работает его методика (и при этом не объясняет, почему необходимы две перегрузки).
Джеймс МакНеллис
Вы имеете в виду «статическую встроенную перегрузку T * f (T * v, int)»? Похоже, это нужно только для обхода Borland C. Подход, используемый там, довольно прост. Единственная тонкая (нестандартная) вещь - это преобразование «T &» в «char &». Несмотря на то, что стандартно разрешает приведение от 'T *' к 'char *', кажется, что нет никаких требований к приведению типов. Тем не менее, можно ожидать, что он будет работать точно так же на большинстве компиляторов.
Константин Ознобихин
@Konstantin: перегрузка используется, потому что для указателя addressofвозвращает сам указатель. Можно спорить, хотел ли пользователь того или нет, но это так, как было указано.
Матье М.
@Matthieu: ты уверен? Насколько я могу судить, любой тип (включая типы указателей) обернут внутри addr_impl_ref, поэтому перегрузка указателя никогда не должна вызываться ...
Matteo Italia
1
@KonstantinOznobihin это на самом деле не отвечает на вопрос, так как все, что вы говорите, - это где искать ответ, а не каков ответ .