Як я можу надійно отримати адресу об’єкта, коли оператор & перевантажений?


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 Uhm, іноді потрібно перевантажити цього оператора і повернути проксі-об'єкт. Хоча зараз я не можу придумати приклад.
Конрад Рудольф

5
@Konrad: Я теж. Якщо вам це потрібно, я б запропонував, що кращим варіантом може бути переосмислення дизайну, оскільки перевантаження цього оператора викликає занадто багато проблем. :)
jalf

2
@Konrad: Приблизно за 20 років програмування на C ++ я одного разу намагався перевантажити цього оператора. Це було на самому початку цих двадцяти років. О, і я не зміг зробити це корисним. Отже, запис оператора, що перевантажує запитання, що часто задає відповідь, говорить: "Одинарна адреса оператора ніколи не повинна бути перевантажена". Ви отримаєте безкоштовне пиво наступного разу, коли зустрінемось, якщо зможете знайти переконливий приклад перевантаження цього оператора. (Я знаю, що ви залишаєте Берлін, тому сміливо можу запропонувати це :))
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 *не відрізняється від функцій. Перший - це перетворення ідентичності, а другий - перетворення функції вказівник, обидва мають ранг "Точна відповідність" (таблиця 13.3.3.1.1).

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

Таким чином, ми просто повертаємо вказівник.

Що станеться, якщо ми передамо тип оператору перетворення?

Якщо оператор конверсії дає a, T*ми маємо неоднозначність: для f(T&,long)інтегрального просування потрібно другий аргумент, тоді як для 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класифікатори, щоб уникнути попереджень компілятора (і належним чином використовувати const_castдля їх видалення).

  • Cast T&доchar const volatile&
  • Смугайте constіvolatile
  • Застосуйте &оператора, щоб взяти адресу
  • Повернення до а T*

const/ volatileЖонглювання трохи чорної магія, але це спростить роботу (а не надання 4 перевантажень). Зауважимо, що оскільки Tце некваліфіковане, якщо ми проходимо а ghost const&, то T*є ghost const*, таким чином кваліфікатори насправді не програли.

EDIT: перевантаження вказівника використовується для вказівника на функції, я дещо змінив вищевказане пояснення. Я досі не розумію, навіщо це потрібно .

Наступний вихід ідеону дещо підсумовує це.


2
"Що станеться, якщо ми передамо вказівник?" частина неправильна. Якщо ми передамо вказівник на якийсь тип U, функція addressof типу "T" вважається "U *", а addr_impl_ref матиме два перевантаження: 'f (U * &, довгий)' і 'f (U **, int) ', очевидно, буде обраний перший.
Костянтин Ознобіхін

@Konstantin: правда, я думав, що два fперевантаження, де функціонують шаблони, тоді як вони є регулярними функціями членів шаблонного класу, дякую, що вказали на це. (Тепер мені просто потрібно розібратися, в чому полягає перевантаження, будь-яка порада?)
Маттьє М.

Це чудова, добре пояснена відповідь. Я начебто подумав, що в цьому є трохи більше, ніж просто "продемонстровано" char*. Дякую, Матьє.
James McNellis

@James: Я мав велику допомогу від @Konstantin, який би ударив мене головою палицею будь-коли, коли я помилився: D
Matthieu M.

3
Чому потрібно обходити типи, які мають функцію перетворення? Чи не віддасть перевагу точне відповідність, ніж викликати будь-яку функцію перетворення T*? EDIT: Зараз я бачу. Було б, але з 0аргументом це закінчилося б перехресним переходом , так було б неоднозначно.
Йоханнес Шауб - ліб

99

Використовуйте std::addressof.

Ви можете подумати, що це робиться за кулісами:

  1. Повторно інтерпретуйте об'єкт як посилання на графік
  2. Візьміть адресу, яка не буде викликати перевантаження
  3. Відкиньте вказівник назад до вказівника вашого типу.

Існуючі реалізації (включаючи Boost.Addressof) роблять саме це, лише доглядаючи constта volatileкваліфікацію.


16
Мені подобається це пояснення краще, ніж вибране, оскільки його можна легко зрозуміти.
Sled

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 &, перевантаження оператора на об'єкт вже не має значення, і ми можемо отримати адресу з вбудованим &оператором.

Реалізація прискорення додає декілька кроків для роботи з об'єктами, що мають кваліфікацію 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. відмінну відповідь .

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


Пояснення Матьє М. щодо передачі покажчика на addressof є невірним. Не псуйте свою чудову відповідь такими правками :)
Костянтин Ознобіхін

"хороший апетит", подальше дослідження показує, що перевантаження викликаються для посилання на функції void func(); boost::addressof(func);. Однак видалення перевантаження не заважає gcc 4.3.4 скласти код і створити той самий вихід, тому я досі не розумію, чому потрібно мати це перевантаження.
Матьє М.

@Matthieu: Схоже, це помилка в gcc. Відповідно до 13.3.3.2 і T &, і T * не відрізняються від функцій. Перший - це перетворення ідентичності, а другий - перетворення функції вказівник, обидва мають ранг "Точна відповідність" (таблиця 13.3.3.1.1). Тому потрібно мати додатковий аргумент.
Костянтин Ознобіхін

@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*>чітко визначено.
curiousguy

2
@curiousguy, і відповідь "так", завжди дозволяється передавати будь-який тип вказівника на [unsigned] char *і, таким чином, читати представлення об'єкта об'єкта, що вказує. Це ще одна сфера, де charє особливі пільги.
підкреслюй_d

@underscore_d Тільки тому, що акторський склад "завжди дозволений", не означає, що ви можете зробити що-небудь із результатом акторського складу.
curiousguy

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 це насправді не дає відповіді на питання, оскільки все, що ви говорите, - де шукати відповідь, а не в чому відповідь .
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.