Учитывая определение Foo
ниже, почему вызов
Foo{2}();
возможно, только если draw(int);
объявлено перед Foo
определением?
Хорошо, по-видимому, очень похожий пример показан в разделе «Шаблоны C++ — Полное руководство» в §14.3.2, и причина примерно в том, что при первом просмотре шаблона любое неквалифицированное зависимое имя (например, draw
в operator()
ниже, которое является зависимым поскольку t
имеет тип параметра шаблона T
). В моем примере в этот момент draw(int)
еще не видно. Позже, когда вызов Foo{2}();
анализируется, выполняется только ADL, поэтому draw(int)
не найден, поскольку набор пространств имен, связанных с int
, пуст.
Но как я понимаю это из стандарта? Полагаю, ответ находится в [basic.lookup], но может ли кто-нибудь помочь мне его расшифровать?
Учитывать
эти заголовки:
draw
для объекта типа шаблона,
// foo.hpp
template<typename T>
struct Foo {
T t;
void operator()() {
draw(t);
}
};
namespace
d со связанной с ним функцией draw
,
// bar.hpp
namespace bar {
struct Bar {};
void draw(Bar const&);
}
draw
для int
s,
// drawInt.hpp
void draw(int);
соответствующие файлы cpp:
// the cpp files
void draw(int) {
std::cout << "int" << std::endl;
}
namespace bar {
void draw(Bar const&){
std::cout << "Bar" << std::endl;
}
}
и main
,
// main.cpp
#include "drawInt.hpp"
#include "bar.hpp"
#include "foo.hpp"
int main() {
Foo{bar::Bar{}}();
Foo{2}();
}
Компиляция и выполнение с помощью
g++ -std=c++20 *.cpp -O0 -o main && ./main
успешно и печатает
Bar
int
вызов void bar::draw(bar::Bar)
возможен благодаря ADL, и нам даже не нужно каким-либо образом объявлять bar::Bar
и bar::draw
перед Foo
, чтобы код работал, т. е. изменялся
#include "bar.hpp"
#include "foo.hpp"
к
#include "foo.hpp"
#include "bar.hpp"
не влияет на программу (я получаю тот же самый двоичный файл). В конце концов, компилятор даже не может осмысленно просмотреть внутреннюю часть operator()
перед строкой
Foo{bar::Bar{}}();
запускает вычет типа шаблона T
.
С другой стороны, вызов void draw(int)
, соответствующий
Foo{2}();
не разрешается через ADL.
Но почему это означает, что объявление перегрузки draw
должно быть видно до определения Foo
, что, в свою очередь, означает, что порядок #include
в main
важен?
Я имею в виду, к тому времени
Foo{2}();
видно, void draw(int)
видно.
И какова бы ни была причина, является ли это признаком плохого дизайна?
Но в чем же тогда преимущество ADL, если я не могу использовать его вместе с вызовами, не относящимися к ADL, для встроенных типов? В этом отношении я думаю об идиоме концепции среды выполнения Шона Парента (вызов ADL — это 35:57 из Better Code: Runtime Polymorphism — Sean Parent.
В общем, вы должны создавать функции ADL только для тех типов параметров, которыми вы владеете или если вы являетесь владельцем вызывающего объекта. Файл с draw(int)
, похоже, не является ни тем, ни другим.
@HolyBlackCat, я понимаю, и теперь я также нашел ссылку на него в шаблонах C++ от Josuttis, но я хотел бы научиться читать/выводить его из стандарта.
Вы указали на раздел в стандарте релевантности. Неполный поиск имени находит предыдущие объявления. Затем применяется поиск, зависящий от аргумента. Поиск имени — это большая тема. Есть ли какое-то конкретное утверждение, которое неясно?
Вопрос «в чем выгода», похоже, не очень хорошо сочетается со всем остальным, это редко встречается у языковых юристов со стандартной ссылкой. Но дело в том, что это средство кастомизации. Тот, кто предоставляет интерфейс отрисовки, реализует его для фундаментальных типов и/или типов библиотек, которыми он владеет или которые имеют смысл. Тем временем пользователь реализует это для своих собственных типов, о которых провайдер отрисовки не может знать.
А как распределяться между реализациями ADL и предоставленными — это большая тема, но вы намекнули на один способ. Заголовок, определяющий Foo, вызывающий отрисовку, может включать заголовок, обеспечивающий отрисовку (для случаев, не связанных с ADL). В любом случае, компромиссы в выборе здесь на самом деле являются их собственным вопросом.
@JeffGarrett, под «какая выгода» я имел в виду то, что draw(b)
выглядит одинаково, независимо от того, является ли b
::bar::Bar
и draw
::bar::draw
или b
является int
и draw
является ::draw
. Так что, когда эти два человека ведут себя по-разному в двух случаях... мне это не кажется самым интуитивным. Я не знал, что встроенные типы и определяемые пользователем типы настолько различаются.
@JeffGarrett, что касается «может включать заголовок», да, может, но стоит ли? foo.hpp
может принадлежать команде (которая также может определять некоторый тип, реализующий draw
, и в этом случае для foo.hpp
имело бы смысл использовать #include
этот заголовок), а другие команды могли бы определять свои собственные типы с соответствующей реализацией интерфейса draw
. . Владельцам foo.hpp
не следует включать эти заголовки, потому что они в принципе даже не знают об их существовании (и самое главное, что они #include "foo.hpp"
). Это "основное" ТУ, которое #include "foo.hpp"
и ссылки на реализаторов.
Команда, владеющая интерфейсом отрисовки, также реализует его для фундаментальных или стандартных типов. Поскольку Foo использует этот интерфейс, он включает в себя этот заголовок. Типы других команд, реализующие отрисовку, реализуют ее в том же заголовке, что и их тип. foo.hpp не должен включать эти заголовки и в этом нет необходимости.
Итак, мой ответ на вопрос: учитывая, что foo.hpp использует draw(), должен ли он включать основной заголовок для draw, который может включать связанные концепции, типы, необходимые для подписи, и включает любые встроенные реализации или реализации по умолчанию, — да.
draw(t)
— это зависимый вызов ([temp.dep.general]/2 ), что делает имя функции зависимым именем в этом контексте. Как указано в [basic.lookup.argdep]/4, когда для зависимого имени выполняется поиск по аргументам, он ищет как в контексте определения, так и в контексте создания экземпляра. Таким образом, пока соответствующая перегрузка draw
объявлена до момента создания экземпляра, ADL может найти ее.
Однако ADL просматривает только связанные пространства имен; он не дублирует поведение обычного поиска неполных имен. Поскольку int
является фундаментальным типом, у него нет связанных пространств имен, поэтому, когда аргумент зависимого вызова имеет тип int
, ADL нечего искать и ничего не находится. Чтобы draw(t)
скомпилировался, когда t
имеет тип int
, draw
должен быть найден с помощью обычного поиска по неполным именам, при котором будут найдены только имена, видимые из контекста определения ([temp.res.general]/1).
В этом нет ничего большего, чем то, что вы уже видите. Неквалифицированные вызовы в шаблонах требуют либо ADL, либо определения, видимого во время объявления шаблона.