Почему функция, объявленная внутри другой функции, не участвует в зависимом от аргумента поиске?
Рассмотрим простой пример:
template <class T>
struct tag { };
int main() {
auto foo = [](auto x) -> decltype(bar(x)) { return {}; };
tag<int> bar(tag<int>);
bar(tag<int>{}); // <- compiles OK
foo(tag<int>{}); // 'bar' was not declared in this scope ?!
}
tag<int> bar(tag<int>) { return {}; }
[Gcc] и [clang] отказываются компилировать код. Является ли этот код неправильно сформированным?
2 ответа
Из неквалифицированных правил поиска ([basic.lookup.unqual]):
Для членов класса
X
имя, используемое в теле функции-члена, [...], должно быть объявлено одним из следующих способов
- еслиX
является локальным классом или является вложенным классом локального класса, до определения классаX
в блоке, содержащем определение классаX
Ваш общий лямбда является локальным классом в main
так использовать bar
, имя bar
должен появиться в декларации заранее.
foo(tag<int>{});
запускает неявную реализацию специализации шаблона функции-члена оператора вызова функции foo
Тип закрытия с аргументом шаблона tag<int>
, Это создает точку реализации для этой специализации шаблона функции-члена. Согласно [temp.point]/1:
Для специализации шаблона функции, специализации шаблона функции- члена или специализации для функции-члена или статического члена данных шаблона класса, если специализация создается неявно, так как на нее ссылаются из другой специализации шаблона и контекста, из которого она создается. ссылка зависит от параметра шаблона, точка создания специализации - точка создания включающей специализации. В противном случае точка создания такой специализации сразу же следует за объявлением или определением области пространства имен, которое ссылается на специализацию.
(акцент мой)
Таким образом, момент создания сразу после main
определение, перед определением пространства имен области bar
,
Поиск имени для bar
используется в decltype(bar(x))
поступает в соответствии с [temp.dep.candidate]/1:
Для вызова функции, где postfix-выражение является зависимым именем, функции-кандидаты находятся с использованием обычных правил поиска (6.4.1, 6.4.2), за исключением того, что:
(1.1) - Для части поиска, использующей неквалифицированный поиск имени (6.4.1), найдены только объявления функций из контекста определения шаблона.
(1.2) - Для части поиска, использующей связанные пространства имен (6.4.2), найдены только объявления функций, найденные либо в контексте определения шаблона, либо в контексте создания шаблона. [...]
Обычный неквалифицированный поиск в контексте определения ничего не находит. ADL в контексте определения тоже ничего не находит. ADL в контексте реализации согласно [temp.point]/7:
Контекст создания выражения, который зависит от аргументов шаблона, представляет собой набор объявлений с внешней связью, объявленных до момента создания специализации шаблона в том же модуле перевода.
Опять ничего, потому что bar
еще не был объявлен в области имен.
Итак, компиляторы верны. Кроме того, обратите внимание на [temp.point]/8:
Специализация для шаблона функции, шаблона функции-члена или функции-члена или статического члена данных шаблона класса может иметь несколько точек создания экземпляров в единице перевода и в дополнение к описанным выше точкам создания экземпляров для любого такого специализация, которая имеет точку создания экземпляра внутри единицы перевода, конец единицы перевода также считается точкой создания экземпляра. Специализация для шаблона класса имеет не более одной точки создания экземпляра в единице перевода. Специализация для любого шаблона может иметь точки создания экземпляров в нескольких единицах перевода. Если две разные точки инстанцирования дают разные значения специализации шаблона в соответствии с правилом единого определения (6.2), программа некорректна, диагностика не требуется.
(акцент мой)
и вторая часть [temp.dep.candidate]/1:
[...] Если вызов будет неверно сформирован или найдет лучшее совпадение, при поиске в связанных пространствах имен будут рассмотрены все объявления функций с внешней связью, введенные в эти пространства имен во всех единицах перевода, а не только те объявления, найденные в определение шаблона и контекст создания экземпляра, то программа имеет неопределенное поведение.
Итак, плохо сформированный NDR или неопределенное поведение - выбирайте сами.
Давайте рассмотрим пример из вашего комментария выше:
template <class T>
struct tag { };
auto build() {
auto foo = [](auto x) -> decltype(bar(x)) { return {}; };
return foo;
}
tag<int> bar(tag<int>) { return {}; }
int main() {
auto foo = build();
foo(tag<int>{});
}
Поиск в контексте определения по-прежнему ничего не находит, но контекст создания сразу после main
определение, поэтому ADL в этом контексте находит bar
в глобальном пространстве имен (связан с tag<int>
) и код компилируется.
Давайте также рассмотрим пример AndyG из его комментария выше:
template <class T>
struct tag { };
//namespace{
//tag<int> bar(tag<int>) { return {}; }
//}
auto build() {
auto foo = [](auto x) -> decltype(bar(x)) { return {}; };
return foo;
}
namespace{
tag<int> bar(tag<int>) { return {}; }
}
int main() {
auto foo = build();
foo(tag<int>{});
}
Опять же, момент создания сразу после main
определение, так почему бы и нет bar
видно? Безымянное определение пространства имен вводит директиву using для этого пространства имен во входящем в него пространстве имен (в данном случае это глобальное пространство имен). Это сделало бы bar
видимый для простого неквалифицированного поиска, но не для ADL согласно [basic.lookup.argdep]/4:
При рассмотрении связанного пространства имен поиск совпадает с поиском, выполняемым, когда связанное пространство имен используется в качестве квалификатора (6.4.3.2), за исключением того, что:
(4.1) - Любые директивы using в связанном пространстве имен игнорируются. [...]
Поскольку в контексте реализации выполняется только часть ADL, bar
в безымянном пространстве имен не видно.
Комментирование нижнего определения и раскомментирование верхнего делает bar
в безымянном пространстве имен, видимом для простого неквалифицированного поиска в контексте определения, поэтому код компилируется.
Давайте также рассмотрим пример из вашего другого комментария выше:
template <class T>
struct tag { };
int main() {
void bar(int);
auto foo = [](auto x) -> decltype(bar(decltype(x){})) { return {}; };
tag<int> bar(tag<int>);
bar(tag<int>{});
foo(tag<int>{});
}
tag<int> bar(tag<int>) { return {}; }
Это принято GCC, но отклонено Clang. Хотя изначально я был совершенно уверен, что это ошибка в GCC, ответ на самом деле может быть не таким четким.
Объявление области видимости блока void bar(int);
отключает ADL в соответствии с [basic.lookup.argdep]/3:
Пусть X будет поисковым набором, созданным безусловным поиском (6.4.1), и пусть Y будет поисковым набором, созданным зависимым от аргумента поиском (определяется следующим образом). Если X содержит
(3.1) - объявление члена класса, или
(3.2) - объявление функции блочной области действия, которое не является объявлением использования, или
(3.3) - объявление, которое не является ни функцией, ни шаблоном функции
тогда Y пуст. [...]
(акцент мой)
Теперь вопрос заключается в том, отключает ли это ADL как в контексте определения, так и в контексте определения, или только в контексте определения.
Если мы считаем ADL отключенным в обоих контекстах, то:
- Объявление области видимости, видимое обычному неквалифицированному поиску в контексте определения, является единственным, видимым для всех экземпляров специализаций шаблона функции-члена типа замыкания. Сообщение об ошибке Clang, что нет жизнеспособного преобразования в
int
, является правильным и обязательным - две приведенные выше цитаты, касающиеся неправильно сформированного отчета о недоставке и неопределенного поведения, не применяются, так как контекст создания экземпляра не влияет на результат поиска имени в этом случае. - Даже если мы переедем
bar
определение пространства имен области вышеmain
код по-прежнему не компилируется по той же причине, что и выше: простой неквалифицированный поиск останавливается, когда он находит объявление области блокаvoid bar(int);
и ADL не выполняется.
Если мы считаем ADL отключенным только в контексте определения, то:
- Что касается контекста реализации, мы вернулись к первому примеру; ADL все еще не может найти определение области имен
bar
, Однако две приведенные выше цитаты (плохо сформированные NDR и UB) применимы, и поэтому мы не можем обвинить компилятор в том, что он не выдал сообщение об ошибке. - перемещение
bar
определение пространства имен области вышеmain
делает код правильно сформированным. - Это также означает, что ADL в контексте создания всегда выполняется для зависимых имен, если только мы не определили, что выражение не является вызовом функции (что обычно включает определение контекста...).
Глядя на то, как сформулирован [temp.dep.candidate] / 1, можно сказать, что простой неквалифицированный поиск выполняется только в контексте определения в качестве первого шага, а затем ADL выполняется в соответствии с правилами в [basic.lookup. argdep] в обоих контекстах в качестве второго шага. Это будет означать, что результат простого неквалифицированного поиска влияет на этот второй шаг в целом, что заставляет меня склоняться к первому варианту.
Кроме того, еще более сильным аргументом в пользу первого варианта является то, что выполнение ADL в контексте создания экземпляров, когда [basic.lookup.argdep]/3.1 или 3.3 применяется в контексте определения, кажется, не имеет смысла.
Тем не менее... может быть стоит спросить об этом на std-обсуждение.
Все цитаты из N4713, текущий стандартный черновик.