Учитывая определение 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);
}
};
namespaced со связанной с ним функцией draw,
// bar.hpp
namespace bar {
struct Bar {};
void draw(Bar const&);
}
draw для ints,
// 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, либо определения, видимого во время объявления шаблона.