Why ADL does not resolve to the correct function with std::get
Asked Answered
O

3

17

I am trying to code a template function that uses an ADL resolved get to fetch members of a struct/range (tuple-esque).

#include <iostream>
#include <utility>
#include <tuple>

int main() {
    auto tup = std::make_tuple(1, 2);
    std::cout << get<0>(tup) << std::endl;
}

I am doing this because of what the structured bindings proposal (http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4659.pdf §11.5.3) says about how get is used to fetch elements from the struct. It says that a non member get is used to fetch elements from within the struct.

I assumed that the code above would compile, because ADL would cause the get function to be looked for in the std namespace (because it's argument is of type std::tuple<int, int>, which is in std), where it would be found. But, I get an error. Can someone explain the right approach here and also why the code above does not work? How can one force ADL to happen in this case?

Orsini answered 3/8, 2017 at 20:19 Comment(1)
P
17

The problem ultimately is templates:

std::cout << get<0>(tup) << std::endl;
//           ~~~~

At that point, the compiler doesn't know that this is a function that needs to be looked up using ADL yet - get is just a name. And since that name by itself doesn't find anything, this is going to be interpreted as an unknown name followed by less-than. To get this to work, you need some other function template get visible:

using std::get;
std::cout << get<0>(tup) << std::endl; // now, OK

Even if it does nothing:

template <class T> void get();

int main() {
    auto tup = std::make_tuple(1, 2); 
    std::cout << get<0>(tup) << std::endl;
}

The structured binding wording explicitly looks up get using argument-dependent lookup, so it avoids the need to have an already-visible function template named get, from [dcl.struct.bind]:

The unqualified-id get is looked up in the scope of E by class member access lookup, and if that finds at least one declaration, the initializer is e.get<i>(). Otherwise, the initializer is get<i>(e), where get is looked up in the associated namespaces. In either case, get<i> is interpreted as a template-id. [ Note: Ordinary unqualified lookup is not performed. — end note ]

The note is the key. If we had performed unqualified lookup, we'd just fail.

Perfidious answered 3/8, 2017 at 20:27 Comment(5)
Very interesting! I wonder though, does introducing this undefined function potentially cause any other problems? Like conflicts or the like?Orsini
@Orsini It would if it ended up being preferred. I mean, don't actually do that - I just put it there for explanatory purposes.Perfidious
hmmm, is there any way I can force ADL to happen? I tried the template keyword but of course that does not work..Orsini
@Orsini Outside of structured bindings, nope. Also, I swear there was a language proposal to make this just work but I can't find.Perfidious
@Barry, it is probably Robert Haberlach's D0389R1 that you remebered, but it was replaced with p0846r0 which was accepted into C++20, which now makes the OP's code validCutinize
S
12

Argument Dependent Lookup doesn't work the same way for function templates where an explicit template argument is given.

Although a function call can be resolved through ADL even if ordinary lookup finds nothing, a function call to a function template with explicitly-specified template arguments requires that there is a declaration of the template found by ordinary lookup (otherwise, it is a syntax error to encounter an unknown name followed by a less-than character)

Basically, there needs to be some way for the unqualified lookup to find a template function. Then, the ADL can kick in (because the name get is then known to be a template). Cppreference gives an example:

namespace N1 {
  struct S {};
  template<int X> void f(S);
}
namespace N2 {
  template<class T> void f(T t);
}
void g(N1::S s) {
  f<3>(s);      // Syntax error (unqualified lookup finds no f)
  N1::f<3>(s);  // OK, qualified lookup finds the template 'f'
  N2::f<3>(s);  // Error: N2::f does not take a non-type parameter
                //        N1::f is not looked up because ADL only works
                //              with unqualified names
  using N2::f;
  f<3>(s); // OK: Unqualified lookup now finds N2::f
           //     then ADL kicks in because this name is unqualified
           //     and finds N1::f
}

Structured bindings are a special case, with ADL enabled.

In the following contexts ADL-only lookup (that is, lookup in associated namespaces only) takes place:

  • the lookup of non-member functions begin and end performed by the range-for loop if member lookup fails
  • the dependent name lookup from the point of template instantiation.
  • the lookup of non-member function get performed by structured binding declaration for tuple-like types

Emphasis added

Search answered 3/8, 2017 at 20:26 Comment(3)
Would it work to say std::cout << template get<0>(tup) << '\n', or does a disambiguating template not work there for some reason?Puett
@DanielH That sounds like it could work if someone proposed it. It doesn't work at the momentSearch
Thanks for the answer. I want to accept both answers but since the other answer includes a straightforward example of a simple resolution technique. I am going to accept that.Orsini
C
2

Fast forward to C++20

p0846r0 that was accepted into C++20 now allows ADL for a call to template function with explicit template arguments.

So the OP's code now compiles as is with C++20 without an error!

Cutinize answered 10/9, 2020 at 13:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.