Почему я должен получить доступ к членам базового класса шаблона через указатель this?

199

Если бы приведенные ниже классы не были шаблонами, я мог бы просто иметь их xв derivedклассе. Тем не менее, с кодом ниже, я должен использовать this->x. Зачем?

template <typename T>
class base {

protected:
    int x;
};

template <typename T>
class derived : public base<T> {

public:
    int f() { return this->x; }
};

int main() {
    derived<int> d;
    d.f();
    return 0;
}
Али
источник
1
Ах, боже. Это как-то связано с поиском имени. Если кто-то не ответит на это в ближайшее время, я посмотрю и выложу (сейчас занят).
templatetypedef
@Ed Swangren: Извините, я пропустил его среди предложенных ответов при публикации этого вопроса. Я долго искал ответ до этого.
Али
6
Это происходит из-за двухфазного поиска имен (который не все компиляторы используют по умолчанию) и зависимых имен. Есть 3 решения этой проблемы, кроме префикса xwith this->, а именно: 1) Использовать префикс base<T>::x, 2) Добавить оператор using base<T>::x, 3) Использовать глобальный переключатель компилятора, который включает разрешающий режим. Плюсы и минусы этих решений описаны в stackoverflow.com/questions/50321788/…
Джордж Робинсон

Ответы:

274

Краткий ответ: чтобы создать xзависимое имя, чтобы поиск откладывался до тех пор, пока не будет известен параметр шаблона.

Длинный ответ: когда компилятор видит шаблон, он должен немедленно выполнить определенные проверки, не видя параметра шаблона. Другие откладываются до тех пор, пока параметр не станет известен. Это называется двухфазной компиляцией, и MSVC этого не делает, но это требуется стандартом и реализуется другими основными компиляторами. Если вам нравится, компилятор должен скомпилировать шаблон, как только он его увидит (для какого-то внутреннего представления дерева разбора), и отложить компиляцию до более поздней версии.

Проверки, которые выполняются над самим шаблоном, а не над его конкретными экземплярами, требуют, чтобы компилятор мог разрешать грамматику кода в шаблоне.

В C ++ (и C) для разрешения грамматики кода иногда необходимо знать, является ли что-то типом или нет. Например:

#if WANT_POINTER
    typedef int A;
#else
    int A;
#endif
static const int x = 2;
template <typename T> void foo() { A *x = 0; }

если A является типом, который объявляет указатель (без эффекта, кроме как для затенения глобального x). Если A - это объект, это умножение (и запрет на перегрузку некоторых операторов недопустимо, присваивая значение r). Если это не так, эта ошибка должна быть диагностирована на этапе 1 , она определяется стандартом как ошибка в шаблоне , а не в какой-то конкретной ее реализации. Даже если шаблон никогда не создается, если A является a, intто приведенный выше код является некорректным и должен быть диагностирован, как если бы это fooбыл не шаблон вообще, а простая функция.

Теперь, согласно стандарту, имена, которые не зависят от параметров шаблона, должны быть разрешены на этапе 1. AЗдесь это не зависимое имя, оно относится к одному и тому же, независимо от типа T. Поэтому его необходимо определить до того, как шаблон будет определен, чтобы его можно было найти и проверить на этапе 1.

T::Aбыло бы имя, которое зависит от T. Мы не можем знать, на этапе 1, является ли это тип или нет. Тип, который в конечном итоге будет использоваться как Tв экземпляре, вполне вероятно, еще даже не определен, и даже если бы это было так, мы не знаем, какой тип (типы) будет использоваться в качестве параметра шаблона. Но мы должны разрешить грамматику, чтобы выполнить наши драгоценные проверки фазы 1 для плохо сформированных шаблонов. Таким образом, в стандарте есть правило для зависимых имен - компилятор должен предполагать, что они не являются типами, если только он не квалифицирован typenameдля указания того, что они являются типами или используются в определенных однозначных контекстах. Например , в template <typename T> struct Foo : T::A {};, T::Aиспользуется в качестве базового класса и , следовательно , однозначно тип. Если Fooсоздается экземпляр с некоторым типом, который имеет член данныхA вместо вложенного типа A это ошибка в коде, выполняющем создание экземпляра (фаза 2), а не ошибка в шаблоне (фаза 1).

Но как насчет шаблона класса с зависимым базовым классом?

template <typename T>
struct Foo : Bar<T> {
    Foo() { A *x = 0; }
};

Является ли зависимое имя или нет? С базовыми классами любое имя может появиться в базовом классе. Таким образом, мы можем сказать, что A является зависимым именем, и рассматривать его как нетиповое. Это может иметь нежелательный эффект, поскольку каждое имя в Foo является зависимым, и, следовательно, каждый тип, используемый в Foo (кроме встроенных типов), должен быть квалифицирован. Внутри Foo вы должны написать:

typename std::string s = "hello, world";

потому std::stringчто будет зависимым именем, и, следовательно, предполагается, что это не тип, если не указано иное. Ой!

Вторая проблема с разрешением вашего предпочтительного кода ( return x;) состоит в том, что даже если Barон определен ранее Fooи xне является членом в этом определении, кто-то может позже определить специализацию Barдля некоторого типа Baz, такого Bar<Baz>как элемент данных x, и затем создать экземпляр Foo<Baz>, Таким образом, в этом случае ваш шаблон будет возвращать элемент данных, а не глобальный x. Или, наоборот, если определение базового шаблона Barимело место x, они могли бы определить специализацию без него, и ваш шаблон мог бы искать глобальное xвозвращение Foo<Baz>. Я думаю, что это было так же удивительно и печально, как и ваша проблема, но это молча удивительно, в отличие от неожиданной ошибки.

Чтобы избежать этих проблем, действующий стандарт гласит, что зависимые базовые классы шаблонов классов просто не рассматриваются для поиска без явного запроса. Это мешает всему быть зависимым только потому, что его можно найти в зависимой базе. Это также имеет нежелательный эффект, который вы видите - вы должны квалифицировать материал из базового класса, или он не найден. Есть три распространенных способа сделать Aзависимость:

  • using Bar<T>::A;в классе - Aтеперь относится к чему-то в Bar<T>, следовательно, зависимым.
  • Bar<T>::A *x = 0;в точке использования - опять же, Aбезусловно, в Bar<T>. Это умножение, поскольку оно typenameне использовалось, поэтому, возможно, это плохой пример, но нам придется подождать до создания экземпляра, чтобы выяснить, operator*(Bar<T>::A, x)возвращает ли значение r. Кто знает, может быть, это так ...
  • this->A;в точке использования - Aчлен, поэтому, если он не включен Foo, он должен быть в базовом классе, опять же, в стандарте говорится, что это делает его зависимым.

Двухфазная компиляция сложна и трудна, и вводит некоторые неожиданные требования для дополнительной словесности в вашем коде. Но, скорее, как и демократия, это, пожалуй, худший из возможных способов, помимо всех остальных.

Вы можете обоснованно утверждать, что в вашем примере return x;не имеет смысла, если xэто вложенный тип в базовом классе, поэтому язык должен (а) сказать, что это зависимое имя и (2) обрабатывать его как не тип, и ваш код будет работать без this->. В какой-то степени вы стали жертвой сопутствующего ущерба от решения проблемы, которая не применима в вашем случае, но все еще существует проблема, связанная с тем, что ваш базовый класс потенциально вводит имена под вами, которые являются теневыми глобалами, или не имеет имен, которые вы считали они имели, и вместо этого был найден глобальный.

Можно также утверждать, что значение по умолчанию должно быть противоположным для зависимых имен (предполагается, что тип, если не указано иное, чтобы быть объектом), или что значение по умолчанию должно быть более контекстно-зависимым (в std::string s = "";, std::stringможет быть прочитано как тип, так как ничто иное не делает грамматическим смысл хоть и std::string *s = 0;неоднозначный). Опять же, я не знаю, как были согласованы правила. Я предполагаю, что количество страниц текста, которые потребуются, сведено к минимуму с созданием множества конкретных правил, для которых контексты принимают тип, а какие нетипизируются.

Стив Джессоп
источник
1
О, хороший подробный ответ. Разъяснил пару вещей, которые я никогда не удосужился посмотреть. :) +1
Джалф
20
@jalf: есть ли такая вещь, как C ++ QTWBFAETYNSYEWTKTAAHMITTBGOW - «Часто задаваемые вопросы, за исключением того, что вы не уверены, что даже хотите знать ответ, и у вас есть более важные вещи, с которыми нужно разобраться»?
Стив Джессоп
4
необычный ответ, интересно, может ли вопрос вписаться в часто задаваемые вопросы.
Матье М.
Ого, мы можем сказать, энциклопедические? highfive Одна тонкая точка, хотя: «Если Foo создается с некоторым типом, который имеет член данных A вместо вложенного типа A, это ошибка в коде, выполняющем создание экземпляра (фаза 2), а не ошибка в шаблоне (фаза 1) «. Может быть, лучше сказать, что шаблон не поврежден, но это все же может быть причиной неправильного предположения или логической ошибки со стороны автора шаблона. Если помеченный экземпляр был на самом деле предполагаемым вариантом использования, то шаблон был бы неправильным.
Ионокласт Бригам
1
@JohnH. Учитывая, что несколько компиляторов реализованы -fpermissiveили похожи, да, это возможно. Я не знаю деталей того, как это реализовано, но компилятор должен отложить разрешение xдо тех пор, пока не узнает фактический базовый класс tempate T. Таким образом, в принципе в недопустимом режиме он может зафиксировать факт, что он отложил его, отложить его, выполнить поиск, как только он произойдет T, и, когда поиск завершится успешно, выдать предложенный вами текст. Это было бы очень точным предложением, если бы оно было сделано только в тех случаях, когда оно работает: шансы, что пользователь имел в виду что-то другое xиз еще одной области, довольно крошечные!
Стив Джессоп
13

(Оригинальный ответ от 10 января 2011 г.)

Я думаю, что нашел ответ: проблема GCC: использование члена базового класса, который зависит от аргумента шаблона . Ответ не является специфичным для gcc.


Обновление: в ответ на комментарий Микаэля , из проекта N3337 стандарта C ++ 11:

14.6.2 Зависимые имена [temp.dep]
[...]
3 В определении класса или шаблона класса, если базовый класс зависит от параметра-шаблона, область действия базового класса не проверяется во время поиска по неквалифицированному имени либо в точка определения шаблона класса или члена или во время создания шаблона класса или члена.

Является ли « потому что стандарт говорит так» , считается, что ответ, я не знаю. Теперь мы можем спросить, почему стандарт предписывает это, но, как отмечают превосходный ответ Стива Джессопа и другие, ответ на этот последний вопрос довольно длинный и спорный. К сожалению, когда речь идет о стандарте C ++, зачастую почти невозможно дать краткое и самостоятельное объяснение того, почему стандарт требует чего-то; это относится и к последнему вопросу.

Али
источник
11

Это xскрыто во время наследования. Вы можете показать через:

template <typename T>
class derived : public base<T> {

public:
    using base<T>::x;             // added "using" statement
    int f() { return x; }
};
chrisaycock
источник
25
Этот ответ не объясняет, почему он скрыт.
Джеймсдлин