Почему компилятор дважды вызывает оператор ->

Следующий код извлекается из: 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? Как можно использовать перегруз?