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

Для доступа к членам базового класса шаблона требуется синтаксис 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 и одновременного использования в C using B<X>::x вместо using A<X>::x дает код, который компилируется.

Является ли код допустимым?

Добавление

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

Ответы [ 4 ]

4 голосов
/ 22 мая 2019

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

[namespace.udecl]

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

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

[temp.res]

9 При поиске объявления имени, используемого в определении шаблона, для независимых имен используются обычные правила поиска ([basic.lookup.unqual], [basic.lookup.argdep]).Поиск имен, зависящих от параметров шаблона, откладывается до тех пор, пока фактический аргумент шаблона не станет известен ([temp.dep]).

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

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> {};

Сделать правильное объявление выше.

2 голосов
/ 22 мая 2019

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

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

Можно ли разрешить шаблонному классу, производному от B, доступ к x только через this-> x, используя B :: x или 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 напрямую.

1 голос
/ 22 мая 2019

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

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>!

Удачи!

0 голосов
/ 23 мая 2019
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 ++ . Многие правильно сформированные ограничения проверяются во время создания экземпляра, и это нормально.

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

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

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

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

// 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) visibleи поиск имени 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(), и компилятор видит, что он не рекурсирует и не компилирует его так, что рекурсия нарушена (этоможет иметь место, если в этой функции есть модифицированная статическая переменная, и компилятор перемещает модификации по вызовам функций для свертывания последовательных модификаций)].

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

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

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

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

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

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

...