Почему компилятор дважды вызывает оператор ->
Следующий код извлекается из: https://github.com/facebook/folly/blob/master/folly/Synchronized.h
Недавно я посмотрел библиотеку Folly и нашел что-то интересное. Рассмотрим следующий пример:
#include <iostream>
struct Lock {
void lock() {
std::cout << "Locking" << std::endl;
}
void unlock() {
std::cout << "Unlocking" << std::endl;
}
};
template <class T, class Mutex = Lock >
struct Synchronized {
struct LockedPtr {
explicit LockedPtr(Synchronized* parent) : p(parent) {
p->m.lock();
}
~LockedPtr() {
p->m.unlock();
}
T* operator->() {
std::cout << "second" << std::endl;
return &p->t;
}
private:
Synchronized* p;
};
LockedPtr operator->() {
std::cout << "first" << std::endl;
return LockedPtr(this);
}
private:
T t;
mutable Mutex m;
};
struct Foo {
void a() {
std::cout << "a" << std::endl;
}
};
int main(int argc, const char *argv[])
{
Synchronized<Foo> foo;
foo->a();
return 0;
}
Вывод:
first
Locking
second
a
Unlocking
Мой вопрос: почему этот код действителен? У этого шаблона есть имя?
Оператор → вызывается дважды, но он был написан только один раз.
Ответы
Ответ 1
Из-за того, что говорит стандарт:
13.5.6 Доступ к члену класса [over.ref]
1) operator->
должна быть нестатическая функция-член, не принимающая параметры. Он реализует доступ к членам класса с помощью ->
postfix-expression -> id-expression
Выражение x->m
интерпретируется как (x.operator->())->m
для объекта класса x
типа T
, если T::operator->()
существует, и если оператор выбран как лучший функция соответствия с помощью механизма разрешения перегрузки (13.3).
(акцент мой)
В вашем случае x
есть foo
и m
is a()
, Теперь Synchronized
перегружает operator->
, foo->a()
эквивалентно:
(foo.operator->())->a();
foo.operator->()
- это ваша перегрузка в классе Synchronized
, которая возвращает LockedPtr
, а затем LockedPtr
вызывает свой собственный operator->
.
Ответ 2
Спросите себя: как еще он может себя вести?
Помните, что точка перегрузки operator->
вообще такова, что класс интеллектуального указателя может использовать тот же синтаксис, что и необработанный указатель. То есть, если у вас есть:
struct S
{
T m;
};
и у вас есть указатель p
до S
, тогда вы получаете доступ к S::m
через p->m
независимо от того, является ли p
типом S*
или некоторым pointer_class<S>
.
Также существует разница между использованием ->
и непосредственным вызовом operator->
:
pointer_class<S> pointerObj;
S* p = pointerObj.operator->();
Обратите внимание, что если использование перегруженного ->
автоматически не опускалось на дополнительный уровень, что может означать p->m
? Как можно использовать перегруз?