В С++ я ищу важные разделы стандарта
объясняя тонкую разницу в поведении, которую я наблюдал между
два оператора доступа к указателю на элемент языка, .*
и ->*
.
Согласно моей тестовой программе, показанной ниже, в то время как ->*
, кажется, позволяет
правое выражение должно быть любого типа, неявно преобразуемого в
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.
Нигде я не нашел понятия преобразования правого операнда.
Вопрос
Что я упустил из виду? Может ли кто-нибудь указать мне объяснение этой разницы в поведении?
@PepijnKramer Случаи использования указателей на член, безусловно, редки. Я помню, как применял их при написании классов-оболочек для компонентов библиотеки C. Чтобы избежать шаблонности, я написал шаблон класса, способный обернуть пару структурно схожих компонентов. Классы политик, используемые вместе с шаблоном, будут описывать внутренние компоненты, необходимые для реализации, в виде псевдонимов типов, указателей на функции и... указателей на члены.
Интересная головоломка. Вы, вероятно, должны иметь тег language-lawyer
на этом вопросе. Есть ли у ->*
«рекурсивное» поведение, которое есть у ->
?
@Элджей Хороший вопрос. Я добавил этот тег.
Я подозреваю, что ответ заключается в том, что (*E1).*E2
для встроенных типов, а Offset
не является встроенным типом. В сочетании с ->*
участвует в разрешении перегрузки с пользовательскими операторами (например, operator M C::* ()
), но .*
нет.
Должно быть, это был интересный фрагмент кода. Приятно знать, что это был какой-то "библиотечный" внутренний материал. :)
Одним из моих ответов было (ab) использование ->*
для расширений функций. Не то, что я рекомендую, но веселое упражнение. stackoverflow.com/a/57081233/4641116
@Eljay У меня возник соблазн проголосовать за ваш связанный ответ, и я просто воздержусь по той же причине, что уже объяснена в вашей редакционной заметке. Как человек, убивающий время с C++, как другие могли бы сделать с головоломкой «1000 частей черного ада», я разумно взволнован. Почувствуйте аплодисменты! :)
Когда используются перегружаемые операторы и хотя бы один операнд имеет тип класса или перечисления, разрешение перегрузки выполняется с использованием набора кандидатов, который включает встроенные кандидаты ([over.match.oper]/3 ) — в частности, для ->*
, см. [over.built]/9.
В этом случае выбирается встроенный кандидат, поэтому ко второму операнду применяется неявное преобразование, а затем ->*
интерпретируется как встроенный оператор ([over.match.oper]/11).
С .*
вообще нет разрешения перегрузки, поэтому нет и неявного преобразования.
Что мне любопытно, так это то, почему вам вообще нужен указатель на член. Я использую C++ уже почти 30 лет и никогда не нуждался в нем. Всегда есть лучший вариант с точки зрения дизайна.