Почему объявление функции глобальной области со встроенным типом arg должно быть видимым перед неквалифицированным вызовом этого имени с аргументом типа шаблона?

вр; доктор

Учитывая определение 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, либо определения, видимого во время объявления шаблона.

HolyBlackCat 22.06.2024 19:35

В общем, вы должны создавать функции ADL только для тех типов параметров, которыми вы владеете или если вы являетесь владельцем вызывающего объекта. Файл с draw(int), похоже, не является ни тем, ни другим.

HolyBlackCat 22.06.2024 19:37

@HolyBlackCat, я понимаю, и теперь я также нашел ссылку на него в шаблонах C++ от Josuttis, но я хотел бы научиться читать/выводить его из стандарта.

Enlico 22.06.2024 20:23

Вы указали на раздел в стандарте релевантности. Неполный поиск имени находит предыдущие объявления. Затем применяется поиск, зависящий от аргумента. Поиск имени — это большая тема. Есть ли какое-то конкретное утверждение, которое неясно?

Jeff Garrett 22.06.2024 20:52

Вопрос «в чем выгода», похоже, не очень хорошо сочетается со всем остальным, это редко встречается у языковых юристов со стандартной ссылкой. Но дело в том, что это средство кастомизации. Тот, кто предоставляет интерфейс отрисовки, реализует его для фундаментальных типов и/или типов библиотек, которыми он владеет или которые имеют смысл. Тем временем пользователь реализует это для своих собственных типов, о которых провайдер отрисовки не может знать.

Jeff Garrett 22.06.2024 20:56

А как распределяться между реализациями ADL и предоставленными — это большая тема, но вы намекнули на один способ. Заголовок, определяющий Foo, вызывающий отрисовку, может включать заголовок, обеспечивающий отрисовку (для случаев, не связанных с ADL). В любом случае, компромиссы в выборе здесь на самом деле являются их собственным вопросом.

Jeff Garrett 22.06.2024 20:59

@JeffGarrett, под «какая выгода» я имел в виду то, что draw(b) выглядит одинаково, независимо от того, является ли b::bar::Bar и draw::bar::draw или b является int и draw является ::draw. Так что, когда эти два человека ведут себя по-разному в двух случаях... мне это не кажется самым интуитивным. Я не знал, что встроенные типы и определяемые пользователем типы настолько различаются.

Enlico 23.06.2024 09:21

@JeffGarrett, что касается «может включать заголовок», да, может, но стоит ли? foo.hpp может принадлежать команде (которая также может определять некоторый тип, реализующий draw, и в этом случае для foo.hpp имело бы смысл использовать #include этот заголовок), а другие команды могли бы определять свои собственные типы с соответствующей реализацией интерфейса draw. . Владельцам foo.hpp не следует включать эти заголовки, потому что они в принципе даже не знают об их существовании (и самое главное, что они #include "foo.hpp"). Это "основное" ТУ, которое #include "foo.hpp" и ссылки на реализаторов.

Enlico 23.06.2024 09:27

Команда, владеющая интерфейсом отрисовки, также реализует его для фундаментальных или стандартных типов. Поскольку Foo использует этот интерфейс, он включает в себя этот заголовок. Типы других команд, реализующие отрисовку, реализуют ее в том же заголовке, что и их тип. foo.hpp не должен включать эти заголовки и в этом нет необходимости.

Jeff Garrett 23.06.2024 15:30

Итак, мой ответ на вопрос: учитывая, что foo.hpp использует draw(), должен ли он включать основной заголовок для draw, который может включать связанные концепции, типы, необходимые для подписи, и включает любые встроенные реализации или реализации по умолчанию, — да.

Jeff Garrett 23.06.2024 15:34
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
10
80
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

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

Другие вопросы по теме