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 ++.