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

Для доступа к членам базового класса шаблона требуется синтаксис this->member или директива using. Распространяется ли этот синтаксис также на базовые классы шаблонов, которые не унаследованы напрямую?

Рассмотрим следующий код:

template <bool X>
struct A {
  int x;
};

template <bool X>
struct B : public A<X> {
  using A<X>::x; // OK even if this is commented out
};

template <bool X>
struct C : public B<X> {
  // using B<X>::x; // OK
  using A<X>::x; // Why OK?
  C() { x = 1; }
};

int main()
{
  C<true> a;

  return 0;
}

Поскольку объявление класса шаблона B содержит using A<X>::x, естественно, производный класс шаблона C может получить доступ к x с помощью using B<X>::x. Тем не менее, на g++ 8.2.1 и clang++ 6.0.1 приведенный выше код компилируется нормально, где доступ к x осуществляется в C с помощью using, который получает x непосредственно из A

Я ожидал, что C не может получить прямой доступ к A. Кроме того, комментирование using A<X>::x в B по-прежнему приводит к компиляции кода. Даже комбинация комментирования using A<X>::x в B и одновременного использования в Cusing B<X>::x вместо using A<X>::x дает код, который компилируется.

Является ли код законным?

Добавление

Чтобы быть более ясным: вопрос возникает о классах шаблон, и речь идет о видимости элементов, унаследованных классами шаблонов. При стандартном публичном наследовании открытые члены A доступны для C, поэтому, используя синтаксис this->x в C, действительно можно получить доступ к A<X>::x. А как же директива using? Как компилятор правильно разрешает using A<X>::x, если A<X> не является прямой основой C?

Почему это должно быть незаконным? Производный класс наследует все члены своего базового класса с одинаковой видимостью. В C++ нет различий в том, был ли член базы определен в самой базе или унаследован — по крайней мере, до тех пор, пока не происходит скрытия из-за переменных с одинаковыми именами в базовом и производном классе, но в этом случае вы все равно можете столкнуться с другими проблемами. .

Aconcagua 22.05.2019 12:14

@Aconcagua спасибо за комментарий, я немного объяснил вопрос.

francesco 22.05.2019 12:56

Это хороший вопрос. A<X> быть базой зависит от B. Это действительно поднимает вопрос, почему можно свободно называть A<X> основой. +1.

StoryTeller - Unslander Monica 22.05.2019 13:01

Спасибо за разъяснения. один голос, так как это заставило меня дважды подумать!

AKL 22.05.2019 14:21

Это не о видимости. Речь идет о том, должен ли член существовать. Например, вы можете добавить template<> A<false> {};, который определяет экземпляр A, в котором нет члена с именем x. Выражение A<X>::x говорит компилятору: «Я ожидаю члена с именем x в A<X>, поэтому, если вы его не найдете, прекратите поиск. В противном случае компилятор продолжит поиск во внешних областях, в конечном итоге найдя этот глобальный int x; и используя его, с удивительным Результаты.

Pete Becker 22.05.2019 17:56

@Aconcagua Поиск (каламбур): поиск имени в два этапа

curiousguy 22.05.2019 20:32

@PeteBecker Речь идет о видимости: как компилятор может знать, что x находится в базовом классе A без объявления использования?

curiousguy 23.05.2019 03:38
3 метода стилизации элементов HTML
3 метода стилизации элементов HTML
Когда дело доходит до применения какого-либо стиля к нашему HTML, существует три подхода: встроенный, внутренний и внешний. Предпочтительным обычно...
Формы c голосовым вводом в React с помощью Speechly
Формы c голосовым вводом в React с помощью Speechly
Пытались ли вы когда-нибудь заполнить веб-форму в области электронной коммерции, которая требует много кликов и выбора? Вас попросят заполнить дату,...
Стилизация и валидация html-формы без использования JavaScript (только HTML/CSS)
Стилизация и валидация html-формы без использования JavaScript (только HTML/CSS)
Будучи разработчиком веб-приложений, легко впасть в заблуждение, считая, что приложение без JavaScript не имеет права на жизнь. Нам становится удобно...
Flatpickr: простой модуль календаря для вашего приложения на React
Flatpickr: простой модуль календаря для вашего приложения на React
Если вы ищете пакет для быстрой интеграции календаря с выбором даты в ваше приложения, то библиотека Flatpickr отлично справится с этой задачей....
В чем разница между Promise и Observable?
В чем разница между Promise и Observable?
Разберитесь в этом вопросе, и вы значительно повысите уровень своей компетенции.
Что такое cURL в PHP? Встроенные функции и пример GET запроса
Что такое cURL в PHP? Встроенные функции и пример GET запроса
Клиент для URL-адресов, cURL, позволяет взаимодействовать с множеством различных серверов по множеству различных протоколов с синтаксисом URL.
8
7
1 028
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Is the code legal?

да. Это то, что делает публичное наследование.

Is it possible to allow a template class derived from B to access to x only via this->x, using B::x or B::x? ...

Вы можете использовать частное наследование (т.е. struct B : private A<X>) и организовать доступ к A<X>::x только через общедоступный/защищенный интерфейс B.

Кроме того, если вы беспокоитесь о скрытых членах, вам следует использовать class вместо struct и явно указать желаемую видимость.


Что касается дополнения, обратите внимание, что:

(1) компилятор знает, на какой объект A<X>::x ссылается заданный экземпляр A<X> (поскольку A определен в глобальной области видимости, а X является параметром шаблона C).

(2) У вас действительно есть экземпляр A<X> - this является понтером для производного класса (не имеет значения, является ли A<X> прямым базовым классом или нет).

(3) Объект A<X>::x виден в текущей области (поскольку наследование и сам объект являются общедоступными).

Оператор using — это просто синтаксический сахар. Как только все типы разрешены, компилятор заменяет последующее использование x соответствующим адресом памяти в экземпляре, мало чем отличаясь от прямого написания this->x.

Спасибо за ответ. Я немного объяснил вопрос и удалил второй как не относящийся к основному вопросу.

francesco 22.05.2019 12:56

Отредактировано с учетом вашего добавления

Benny K 22.05.2019 13:41

Возможно, этот пример может дать вам некоторое представление о том, почему это должно быть законным:

template <bool X>
struct A {
  int x;
};

template <bool X>
struct B : public A<X> {
  int x;
};

template <bool X>
struct C : public B<X> {
  //it won't work without this
  using A<X>::x; 
  //or
  //using B<X>::x;
  C() {  x = 1; }
  // or
  //C() { this -> template x = 1; }
  //C() { this -> x = 1; }
};

В случае выбора C() { this -> template x = 1; } последний унаследованный x (B::x) будет присвоен 1, а не A::x.

Его можно просто проверить:

    C<false> a;
    std::cout << a.x    <<std::endl;
    std::cout << a.A::x <<std::endl;
    std::cout << a.B::x <<std::endl;

Предполагая, что программист struct B не знал о членах struct A, но программист struct c знал о членах обоих, кажется вполне разумным разрешить эту функцию!

Что касается того, почему компилятор должен уметь распознавать using A<X>::x;, когда он используется в C<X>, учтите тот факт, что в определении класса/шаблона класса все прямые/косвенные унаследованные базы видны независимо от типа наследования. Но доступны только публично унаследованные!

Например, если это было так:

using A<true>::x;
//or
//using B<true>::x;

Тогда возникла бы проблема:

C<false> a;

Или мудрый наоборот. поскольку ни A<true>, ни B<true> не являются основой для C<false>, поэтому видимы. Но так как это так:

using A<X>::x;

Поскольку общий термин X используется для определения термина A<X>, он сначала выводим, а затем распознаваем, поскольку любой C<X> (если он не будет специализирован позже) косвенно основан на A<X>!

Удачи!

спасибо за ответ, вопрос больше о видимости, см. мой отредактированный вопрос.

francesco 22.05.2019 12:57
this -> template x. Почему template? x нет.
Jarod42 22.05.2019 13:46

@ Jarod42, ты имеешь в виду, почему бы просто не использовать this->x? ты прав. который также можно использовать с тем же эффектом без какой-либо разницы. Я отредактировал свой ответ соответствующим образом и спасибо за совет.

AKL 22.05.2019 13:51

Я бы просто удалил templatethis->x достаточно.

Jarod42 22.05.2019 13:52

@ Jarod42, спасибо, я уже отредактировал свой ответ, включив его. Остальное нормально?

AKL 22.05.2019 13:57
Ответ принят как подходящий

Вы используете A<X> там, где ожидается базовый класс.

[namespace.udecl]

3 In a using-declaration used as a member-declaration, each using-declarator's nested-name-specifier shall name a base class of the class being defined.

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

[temp.res]

9 When looking for the declaration of a name used in a template definition, the usual lookup rules ([basic.lookup.unqual], [basic.lookup.argdep]) are used for non-dependent names. The lookup of names dependent on the template parameters is postponed until the actual template argument is known ([temp.dep]).

Так что это разрешено из-за того, что компилятор не может знать лучше. Он проверит объявление использования при создании экземпляра класса. Действительно, туда можно поместить любой зависимый тип:

template<bool> struct D{};

template <bool X>
struct C : public B<X> {
  using D<X>::x; 
  C() { x = 1; }
}; 

Это не будет проверено, пока значение X не будет известно. Потому что B<X> может принести с собой всевозможные сюрпризы, если он специализирован. Например, можно было бы сделать это:

template<>
struct D<true> { char x; };

template<>
struct B<true> : D<true> {};

Сделать вышеуказанное заявление правильным.

это также будет нормально в случае C<false> c; ?

AKL 22.05.2019 14:03

@AKL - Используете D? Вы получите ошибку при создании экземпляра. Дело в том, что компилятор не может узнать, правильно это или нет, до создания экземпляра.

StoryTeller - Unslander Monica 22.05.2019 14:06

Согласен, дальше спора нет! :) Также один голос за хороший трюк!

AKL 22.05.2019 14:14

"Поскольку это появляется там, где ожидается тип класса, он известен и считается типом" Что может быть A<T> кроме типа?

curiousguy 29.05.2019 18:08
template <bool X>
struct C : public B<X> {
  // using B<X>::x; // OK
  using A<X>::x; // Why OK?
  C() { x = 1; }
};

Вопрос в том, почему это не будет поддерживаться? Потому что ограничение, что A<X> является основой специализации определения основного шаблона C, является вопросом, на который можно ответить только и который имеет смысл только для конкретного аргумента шаблона X?

Возможность проверять шаблоны во время определения никогда не была целью разработки C++.. Многие ограничения правильности проверяются во время создания экземпляра, и это нормально.

[Без истинной концепции (необходимых и достаточных контрактов параметров шаблона) ни один вариант С++ не будет работать значительно лучше, а С++, вероятно, слишком сложен и нерегулярен, чтобы когда-либо иметь настоящие концепции и настоящую отдельную проверку шаблонов.]

Принципы, которые делают необходимым уточнять имя, чтобы сделать его зависимым, имеют ли нет что-либо с ранней диагностикой ошибок в коде шаблона; способ поиска имени в шаблоне дизайнеры сочли необходимым поддерживать «нормальный» (на самом деле чуть менее безумный) поиск имени в коде шаблона.: использование нелокального имени в шаблоне не должно связывать слишком часто с именем, объявленным клиентским кодом, так как это нарушит инкапсуляцию и локальность.

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

Рассмотрим этот «системный» (т.е. не являющийся частью текущего проекта) заголовок:

// useful_lib.hh _________________
#include <basic_tool.hh>

namespace useful_lib {
  template <typename T>
  void foo(T x) { ... }

  template <typename T>
  void bar(T x) { 
    ...foo(x)... // intends to call useful_lib::foo(T)
                 // or basic_tool::foo(T) for specific T
  }
} // useful_lib

И этот код проекта:

// user_type.hh _________________
struct UserType {};

// use_bar1.cc _________________
#include <useful_lib.hh>
#include "user_type.hh"

void foo(UserType); // unrelated with basic_tool::foo

void use_bar1() {
  bar(UserType()); 
}

// use_bar2.cc _________________
#include <useful_lib.hh>
#include "user_type.hh"

void use_bar2() {
  bar(UserType()); // ends up calling basic_tool::foo(UserType)
}

void foo(UserType) {}

Я думаю, что этот код вполне реалистичен и разумен; посмотрите, видите ли вы очень серьезную и нелокальную проблему (проблему, которую можно найти, только прочитав две или более отдельных функций).

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

void use_bar1() {
  bar(UserType()); // ends up calling ::foo(UserType)
}

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

Если этого недостаточно, теперь рассмотрите возможность связывания use_bar1.cc и use_bar2.cc; теперь у нас есть два варианта использования одной и той же функции шаблона в разных контекстах, что приводит к разным расширениям (в терминах макросов, поскольку шаблоны лишь немногим лучше прославленных макросов); в отличие от макросов препроцессора, вам не разрешено делать это, так как одна и та же конкретная функция bar(UserType) определяется двумя разными способами двумя единицами перевода: это нарушение ODR, программа неправильно сформирована, диагностика не требуется. Это означает, что если реализация не перехватывает ошибку во время компоновки (а это делают очень немногие), поведение во время выполнения не определено с самого начала: ни один запуск программы не имеет определенного поведения.

Если вам интересно, дизайн поиска имени в шаблоне в эпоху "ARM" (аннотированное справочное руководство по C++), задолго до стандартизации ISO, обсуждается в D&E (дизайн и развитие C++).

Такого непреднамеренного связывания имени удалось избежать, по крайней мере, с квалифицированными именами и независимыми именами. Вы не можете воспроизвести эту проблему с независимыми неквалифицированными именами:

namespace useful_lib {
  template <typename T>
  void foo(T x) { ... }

  template <typename T>
  void bar(T x) { 
    ...foo(1)... // intends to call useful_lib::foo<int>(int)
  }
} // useful_lib 

Здесь привязка имени выполняется таким образом, что никакое лучшее совпадение перегрузки (т. е. не совпадение с функцией, не являющейся шаблоном) не может «превзойти» специализацию useful_lib::foo<int>, потому что имя связано в контексте определения функции шаблона, а также потому, что useful_lib::foo скрывает любые внешнее имя.

Обратите внимание, что без пространства имен useful_lib можно было бы найти другое foo, которое было объявлено в другом заголовке, включенном ранее:

// some_lib.hh _________________
template <typename T>
void foo(T x) { }

template <typename T>
void bar(T x) { 
  ...foo(1)... // intends to call ::foo<int>(int)
}

// some_other_lib.hh _________________
void foo(int);

// user1.cc _________________
#include <some_lib.hh>
#include <some_other_lib.hh>

void user1() {
  bar(1L);
}

// user2.cc _________________
#include <some_other_lib.hh>
#include <some_lib.hh>

void user2() {
  bar(2L);
}

Вы можете видеть, что единственное декларативное различие между TU — это порядок включения заголовков:

  • user1 вызывает экземпляр bar<long>, определенный без видимого foo(int), а поиск имени foo находит только подпись template <typename T> foo(T), поэтому привязка, очевидно, выполняется к этому шаблону функции;

  • user2 вызывает экземпляр bar<long>, определенный с помощью foo(int), видимым, поэтому поиск имени находит как foo, так и нешаблонный вариант, который лучше подходит; Интуитивное правило перегрузки состоит в том, что выигрывает все (шаблон функции или обычная функция), которое может соответствовать меньшему количеству списков аргументов: foo(int) может соответствовать только int, а template <typename T> foo(T) может соответствовать чему угодно (которое можно скопировать).

Таким образом, соединение обоих TU снова приводит к нарушению ODR; наиболее вероятное практическое поведение состоит в том, что функция, включенная в исполняемый файл, непредсказуема, но оптимизирующий компилятор может предположить, что вызов в user1() не вызывает foo(int) и сгенерирует не встроенный вызов bar<long>, который оказывается вторым экземпляром, который заканчивается вызов foo(int), что может привести к генерации неправильного кода [предположим, что foo(int) может выполнять рекурсию только через user1(), а компилятор видит, что он не рекурсирует, и компилирует его таким образом, что рекурсия прерывается (это может быть в случае, если есть модифицированная статическая переменная в эта функция, и компилятор перемещает модификации между вызовами функций, чтобы свернуть последовательные модификации)].

Это показывает, что шаблоны ужасно слабые и хрупкие, и их следует использовать с особой осторожностью.

Но в вашем случае такой проблемы с привязкой имен нет, так как в этом контексте объявление использования может называть только базовый класс (прямой или косвенный). Не имеет значения, что компилятор не может знать во время определения, является ли это прямой или косвенной базой или ошибкой; он проверит это в свое время.

Хотя ранняя диагностика изначально ошибочного кода разрешена (поскольку sizeof(T()) точно такой же, как sizeof(T), объявленный тип s недопустим в любом экземпляре):

template <typename T>
void foo() { // template definition is ill formed
  int s[sizeof(T) - sizeof(T())]; // ill formed
}

диагностика этого во время определения шаблона практически не важна и не требуется для соответствующих компиляторов (и я не верю, что авторы компиляторов пытаются это сделать).

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

Другие вопросы по теме