Разница в поведении операторов доступа указателя на член

В С++ я ищу важные разделы стандарта объясняя тонкую разницу в поведении, которую я наблюдал между два оператора доступа к указателю на элемент языка, .* и ->*.

Согласно моей тестовой программе, показанной ниже, в то время как ->*, кажется, позволяет правое выражение должно быть любого типа, неявно преобразуемого в pointer to member of S, .* не так. При компиляции с gcc и clang, оба компилятора выдают ошибки для строки с пометкой "(2)" заявив, что мой класс Offset нельзя использовать в качестве указателя на член.

Программа испытаний https://godbolt.org/z/46nMPvKxE

#include <iostream>

struct S { int m; };

template<typename C, typename M>
struct Offset
{
    M C::* value;
    operator M C::* () { return value; }  // implicit conversion function
};

int main()
{
    S s{42};
    S* ps = &s;
    Offset<S, int> offset{&S::m};

    std::cout << ps->*offset << '\n';  // (1) ok
    std::cout << s.*offset << '\n';    // (2) error
    std::cout.flush();
}

Вывод компилятора

GCC 12.2:
    'offset' cannot be used as a member pointer, since it is of type 'Offset<S, int>'
clang 15.0:
    right hand operand to .* has non-pointer-to-member type 'Offset<S, int>'

Вариант программы

Чтобы доказать, что ->* действительно выполняет неявное преобразование, используя Offset функцию преобразования в тестовой программе, показанной выше, я объявил ее явной для целей тестирования,

    explicit operator M C::* () { return value; }  // no longer implicit conversion function

в результате компиляторы также выдают ошибки для строки с пометкой «(1)»:

GCC 12.2:
    error: no match for 'operator->*' (operand types are 'S*' and 'Offset<S, int>')
    note: candidate: 'operator->*(S*, int S::*)' (built-in)
    note:   no known conversion for argument 2 from 'Offset<S, int>' to 'int S::*'
clang 15.0:
    error: right hand operand to ->* has non-pointer-to-member type 'Offset<S, int>'

Исследовать

Хотя между двумя операторами существует хорошо задокументированная разница в том, что ->* перегружается, а .* нет, мой код явно не использует эту опцию а скорее полагается на встроенный operator ->*, определенный для необработанного типа указателя S*.

Помимо различий в перегрузочной способности, я просто нашел документацию, в которой говорится о сходстве выражений. Цитата из стандарта (https://open-std.org/jtc1/sc22/wg21/docs/papers/2020/n4868.pdf):

[7.6.4.2] Бинарный оператор .* связывает свой второй операнд, который должен иметь тип «указатель на член T», к его первому операнду. операнд, который должен быть значением gl класса T или класса, однозначной и доступной базой которого является T сорт. Результатом является объект или функция типа, указанного вторым операндом.

[7.6.4.3] [...] Выражение E1->E2 преобразуется в эквивалентную форму ((E1)).*E2.

И цитата из cppreference.com (https://en.cppreference.com/w/cpp/language/operator_member_access#Built-in_pointer-to-member_access_operators):

Второй операнд обоих операторов представляет собой выражение типа указатель на член (данные или функцию) T или указатель на член однозначного и доступного базового класса B T. Выражение E1->*E2 точно эквивалентно (*E1).*E2 для встроенных типов; именно поэтому следующие правила относятся только к E1.*E2.

Нигде я не нашел понятия преобразования правого операнда.

Вопрос

Что я упустил из виду? Может ли кто-нибудь указать мне объяснение этой разницы в поведении?

Что мне любопытно, так это то, почему вам вообще нужен указатель на член. Я использую C++ уже почти 30 лет и никогда не нуждался в нем. Всегда есть лучший вариант с точки зрения дизайна.

Pepijn Kramer 04.01.2023 19:13

@PepijnKramer Случаи использования указателей на член, безусловно, редки. Я помню, как применял их при написании классов-оболочек для компонентов библиотеки C. Чтобы избежать шаблонности, я написал шаблон класса, способный обернуть пару структурно схожих компонентов. Классы политик, используемые вместе с шаблоном, будут описывать внутренние компоненты, необходимые для реализации, в виде псевдонимов типов, указателей на функции и... указателей на члены.

ngmr80 04.01.2023 20:37

Интересная головоломка. Вы, вероятно, должны иметь тег language-lawyer на этом вопросе. Есть ли у ->* «рекурсивное» поведение, которое есть у ->?

Eljay 04.01.2023 20:58

@Элджей Хороший вопрос. Я добавил этот тег.

ngmr80 04.01.2023 21:02

Я подозреваю, что ответ заключается в том, что (*E1).*E2 для встроенных типов, а Offset не является встроенным типом. В сочетании с ->* участвует в разрешении перегрузки с пользовательскими операторами (например, operator M C::* ()), но .* нет.

Eljay 04.01.2023 21:24

Должно быть, это был интересный фрагмент кода. Приятно знать, что это был какой-то "библиотечный" внутренний материал. :)

Pepijn Kramer 05.01.2023 05:02

Одним из моих ответов было (ab) использование ->* для расширений функций. Не то, что я рекомендую, но веселое упражнение. stackoverflow.com/a/57081233/4641116

Eljay 05.01.2023 13:04

@Eljay У меня возник соблазн проголосовать за ваш связанный ответ, и я просто воздержусь по той же причине, что уже объяснена в вашей редакционной заметке. Как человек, убивающий время с C++, как другие могли бы сделать с головоломкой «1000 частей черного ада», я разумно взволнован. Почувствуйте аплодисменты! :)

ngmr80 05.01.2023 17:42
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
4
8
118
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Когда используются перегружаемые операторы и хотя бы один операнд имеет тип класса или перечисления, разрешение перегрузки выполняется с использованием набора кандидатов, который включает встроенные кандидаты ([over.match.oper]/3 ) — в частности, для ->* , см. [over.built]/9.

В этом случае выбирается встроенный кандидат, поэтому ко второму операнду применяется неявное преобразование, а затем ->* интерпретируется как встроенный оператор ([over.match.oper]/11).

С .* вообще нет разрешения перегрузки, поэтому нет и неявного преобразования.

Другие вопросы по теме