Видимость члена класса в подписной декларации функции участника

Почему это работает:

template <typename A>
struct S {
    A a;
    template <typename B>
    auto f(B b) ->
        decltype(a.f(b))
    {
    }
};

Но это не (a и f местами смены):

template <typename A>
struct S {
    template <typename B>
    auto f(B b) ->
        decltype(a.f(b))
    {
    }
    A a;
};

говоря, что a не объявляется в этой области (внутри decltype), но добавление явного this-> заставляет его работать.

Ответ 1

template <typename A>
struct S {
    A a;
    template <typename B>
    auto f(B b) ->
        decltype(a.f(b))
    {
    }
};

Это работает, потому что в концевом возвращаемом типе видны члены окружающего класса. Не все члены, а только те члены, которые были объявлены до него (в концевом типе возврата класс не считается полным, в отличие от тел функции). Итак, что здесь делается:

  • Как мы находимся в шаблоне, выполняется поиск, зависящий от того, зависит ли a или нет. Поскольку a был объявлен до f, a обнаруживается, что он относится к члену класса.
  • По правилам шаблона в С++ обнаруживается, что a относится к члену текущего экземпляра, поскольку он является членом экземпляров окружающего шаблона. В С++ это понятие используется главным образом для определения того, зависят ли имена: если имя известно как относящееся к окружающим членам шаблона, необязательно нужно искать при создании экземпляра, поскольку компилятор уже знает код шаблона (который используется в качестве основы для экземпляра класса, созданного из него!). Рассмотрим:

    template<typename T>
    struct A {
      typedef int type;
      void f() {
        type x;
        A<T>::type y;
      }
    };
    

В С++ 03 вторая строка, объявляющая y, будет ошибкой, потому что A<T>::type является зависимым именем и ему нужно typename перед ним. Только первая линия была принята. В С++ 11 эта несогласованность была исправлена, и оба типа имен не зависят и не нуждаются в typename. Если вы измените typedef на typedef T type;, то оба объявления, x и y будут использовать зависимый тип, но ни один из них не понадобится typename, потому что вы все еще называете членом текущего экземпляра, и компилятор знает, что вы называете тип.

  • Итак, a является членом текущего экземпляра. Но он зависит, потому что тип, используемый для его объявления (a), зависит. Однако это не имеет значения в вашем коде. Независимо от того или нет, a найден и код действителен.

template <typename A>
struct S {
    template <typename B>
    auto f(B b) ->
        decltype(a.f(b))
    {
    }
    A a;
};

В этом коде снова a проверяется, зависит ли он и/или является ли он членом текущего экземпляра. Но так как мы узнали выше, что члены, объявленные после того, как тип возвращаемого возврата не отображается, нам не удается найти объявление для a. В С++, помимо понятия "член текущей экземпляра", существует другое понятие:

  • член неизвестной специализации. Это понятие используется для обозначения случая, когда имя может ссылаться на член класса, который зависит от параметров шаблона. Если бы мы получили доступ к B::a, то a был бы членом неизвестной специализации, потому что неизвестно, какие объявления будут видны при замене B при создании экземпляра.

  • не является ни членом текущего, ни членом неизвестной специализации. Это относится ко всем другим именам. Ваш случай подходит здесь, поскольку известно, что a никогда не может быть членом какого-либо экземпляра при возникновении экземпляра (помните, что поиск по имени не может найти a, поскольку он объявлен после f).

Так как a не зависит от какого-либо правила, поиск, который не нашел никакого объявления, является обязательным, то есть нет другого поиска в экземпляре, который мог бы найти объявление. Не зависящие от имени имена ищут время определения шаблона. Теперь GCC по праву дает вам ошибку (но обратите внимание, что, как всегда, необработанный шаблон не требуется для немедленного диагноза).


template <typename A>
struct S {
    template <typename B>
    auto f(B b) ->
        decltype(this->a.f(b))
    {
    }
    A a;
};

В этом случае вы добавили this и GCC. Имя a, которое следует за this-> снова, - это поиск, чтобы узнать, может ли он быть членом текущего экземпляра. Но опять же из-за видимости члена в возвращаемых типах возврата не найдено объявления. Следовательно, имя считается не являющимся членом текущего экземпляра. Поскольку в экземпляре нет способа, чтобы S мог иметь дополнительные члены, которые могли бы соответствовать a (нет базовых классов S, которые зависят от параметров шаблона), имя также не является членом неизвестной специализации,

Снова С++ не имеет правил, чтобы сделать this->a зависимым. Однако он использует this->, поэтому имя должно ссылаться на некоторый член S при его создании! Поэтому в С++ Standard говорится

Аналогично, если выражение id в выражении доступа к члену класса, для которого тип выражения объекта является текущим экземпляром, не относится к члену текущего экземпляра или к члену неизвестной специализации, программа болен -форменное, даже если шаблон, содержащий выражение доступа к члену, не создается; не требуется диагностика.

Опять же, для этого кода не требуется диагностика (и GCC на самом деле его не дает). Идентификатор-выражение a в выражении доступа к члену this->a зависел в С++ 03, потому что правила в этом стандарте не были так подробно разработаны и точно настроены, как в С++ 11. На мгновение предположим, что у С++ 03 были decltype и возвращающие возвращаемые типы. Что это значит?

  • Поиск будет отложен до создания экземпляра, потому что this->a будет зависимым
  • Поиск в экземпляре, скажем, S<SomeClass> завершится неудачно, потому что this->a не будет найден во время создания экземпляра (как мы уже говорили, возвращающие возвращаемые типы не видят участников, объявленных позже).

Следовательно, раннее отклонение этого кода на С++ 11 является хорошим и полезным.

Ответ 2

Тело функции-члена компилируется так, как если бы оно было определено после класса. Поэтому все, объявленные в классе, находятся в области видимости в этой точке.

Однако объявление функции все еще находится внутри объявления класса и может видеть только предшествующие им имена.

template <typename A>
struct S {
    template <typename B>
    auto f(B b) ->
        decltype(a.f(b)); // error - a is not visible here

    A a;
};

template <typename A>
template <typename B>
    auto S<A>::f(B b) ->
        decltype(a.f(b))
    {
        return a.f(b);   // a is visible here
    }

Ответ 3

В стандарте говорится (раздел 14.6.2.1):

Если для заданного набора аргументов шаблона создается экземпляр специализации шаблона, который ссылается на элемент текущего экземпляра с выражением доступа к квалифицированному идентификатору или члену класса, имя в члене с квалифицированным идентификатором или классом выражение доступа просматривается в контексте создания экземпляра шаблона.

this->a - это выражение доступа к классу, поэтому это правило применяется, и поиск выполняется в момент создания экземпляра, где S<A> завершен.


Наконец, это не решит вашу проблему вообще, потому что в разделе 5.1.1 говорится:

Если объявление объявляет функцию-член или шаблон функции-члена класса X, выражение this - это значение типа "указатель на cv-qualifier-seq X" между необязательным cv-квалификатором -seq и конец описания функции, объявления-участника или декларатора. Он не должен появляться перед необязательным cv-qualifier-seq и он не должен появляться в объявлении статической функции-члена (хотя его тип и категория значения определены в статической функции-члене, поскольку они находятся в нестатической функции-члене).

Таким образом, вы не можете использовать this-> здесь, так как он находится перед частью объявления cv-qualifier-seq объявления.

Подождите, нет! В разделе 8.4.1 говорится:

Декларатор в определении функции должен иметь вид

D1 ( parameter-declaration-clause ) cv-qualifier-seq opt ref-qualifier opt exception-specification opt attribute-specifier-seq opt trailing-return-type opt