Search code examples
c++language-lawyerc++20header-filesargument-dependent-lookup

Why global-scope function declaration with built-in type arg must be visible before unqualified call to that name with argument of template type?


tl;dr

Given the definition of Foo below, why a call to

Foo{2}();

is possible only if draw(int); is declared before Foo definition?

Non- answer

Ok, apparently a very similar example is shown in C++ Templates - The Complete Guide in §14.3.2, and the reason is roughly that when a tempalate is first seen, any unqualified dependent name (e.g. the draw in operator() below, which is dependent because t is of type the template parameter T) is looked up. In my example, at that point, draw(int) is not visible yet. Later, when the call Foo{2}(); is parsed, only ADL is peformed, and so draw(int) is not found because the set of namespaces associated to int is empty.

But how do I understand this from the standard? I suppose the answer is in [basic.lookup], but can anybody guide me to decypher it?

Longer version

Consider

  • these headers:

    • one exposes a class template with a call operator (or other member function, fwiw) calling an unqualified draw function on an object of the template type,
      // foo.hpp
      template<typename T>
      struct Foo {
          T t;
          void operator()() {
              draw(t);
          }
      };
      
    • one exposes a namespaced class, with associated draw function,
      // bar.hpp
      namespace bar {
      struct Bar {};
      void draw(Bar const&);
      }
      
    • one exposes a global-scope draw for ints,
      // drawInt.hpp
      void draw(int);
      
  • the corresponding cpp files:

    // the cpp files
    void draw(int) {
        std::cout << "int" << std::endl;
    }
    namespace bar {
    void draw(Bar const&){
        std::cout << "Bar" << std::endl;
    }
    }
    
  • and the main,

    // main.cpp
    #include "drawInt.hpp"
    #include "bar.hpp"
    #include "foo.hpp"
    int main() {
        Foo{bar::Bar{}}();
        Foo{2}();
    }
    

Compiling and executing with

g++ -std=c++20 *.cpp -O0 -o main && ./main

is successful and prints

Bar
int

the call to void bar::draw(bar::Bar) is possible because of ADL, and we don't even really need bar::Bar and bar::draw be declared in any way before Foo for the code to work, i.e. changing

#include "bar.hpp"
#include "foo.hpp"

to

#include "foo.hpp"
#include "bar.hpp"

has no effect on the program (I get the same exact binary). After all, the inside of operator() can't be even be meaningfully looked at by the compiler, before the line

Foo{bar::Bar{}}();

triggers template type deduction of T.

On the other hand, the call to void draw(int) that corresponds to

Foo{2}();

is not resolved via ADL.

But why does that imply that the declaration of that overload of draw must be visible before the definition of Foo, which in turns means that the order of #includes in main because important?

I mean, by the time

Foo{2}();

is seen, void draw(int) is visible.

And, whatever the reason is, is that a symptom of bad design?

But then what's the benefit of ADL, if I can't use it seamlessly together non-ADL calls for builtin types? In this respect, I'm thinking about Sean Parent runtime concept idiom (the ADL call is 35:57 of Better Code: Runtime Polymorphism - Sean Parent.


Solution

  • draw(t) is a dependent call ([temp.dep.general]/2), which makes the function name a dependent name in this context. As specified in [basic.lookup.argdep]/4, when argument-dependent lookup is performed for a dependent name, it searches both in the definition context and the instantiation context. Thus, as long as the appropriate draw overload is declared before the point of instantiation, it can be found by ADL.

    However, ADL only looks in associated namespaces; it doesn't duplicate the behaviour of ordinary unqualified name lookup. Because int is a fundamental type, it has no associated namespaces, so when the argument to a dependent call has type int, there is nothing for ADL to look in, and nothing is found. In order for draw(t) to compile when t is of type int, draw must be found by ordinary unqualified name lookup, which will only find names that are visible from the definition context ([temp.res.general]/1).