Why doesn't function declared inside other function participate in argument dependent lookup?
Asked Answered
C

2

7

Consider a simple example:

template <class T>
struct tag { };

int main() {
    auto foo = [](auto x) -> decltype(bar(x)) { return {}; };
    tag<int> bar(tag<int>);
    bar(tag<int>{}); // <- compiles OK
    foo(tag<int>{}); // 'bar' was not declared in this scope ?!
}

tag<int> bar(tag<int>) { return {}; }

Both [gcc] and [clang] refuses to compile the code. Is this code ill-formed in some way?

Caaba answered 2/2, 2018 at 15:55 Comment(10)
Yeah, I believe the fwd declaration should appear before the generic lambda.Anklebone
@Anklebone so why does this compile then?Caaba
@Anklebone In fact it needs to be declared outside main() to get it working:Sample1, Sample2Squirearchy
I don't see how ADL applies here. You don't even have a namespace.Jagannath
@BaummitAugen ammm what do you mean? tag<int> is in global namespace isn't it?Caaba
@Caaba Well, yes, but the global namespace is considered here anyways. ADL is for adding other namespaces to be considered based on the namespace of the arguments, isn't it? Looks like ordinary unqualified lookup to me, not affected by the extra rules of ADL.Jagannath
Eh, maybe my wording is off actually. Not sure.Jagannath
@BaummitAugen well I think you might be right... However I don't feel in position to prejudge... :) ADL always overwhelms me...Caaba
@Caaba because of unqualified lookup/scoping haha :-) Check this out comment the bottom anonymous namespace and then uncomment the above one.Anklebone
@Anklebone do it boils to the unqualified lookup. That's what I have missed... Thanks!Caaba
A
5

From unqualified lookup rules ([basic.lookup.unqual]):

For the members of a class X, a name used in a member function body, [...], shall be declared in one of the following ways
— if X is a local class or is a nested class of a local class, before the definition of class X in a block enclosing the definition of class X

Your generic lambda is a local class within main, so to use bar, the name bar must appear in a declaration beforehand.

Anklebone answered 2/2, 2018 at 17:4 Comment(5)
I'm quite confused as following the paragraph I think this shouldn't also compile, should it?Caaba
@Caaba ADL ftw! Andy's cite means the name has to exist - it doesn't mean that that's the one that ends up being called.Murat
@Murat ahhh now everything make sense. Thanks!Caaba
I don't know whether this convinces me. For example this is not rejected auto f() { return [](auto x) -> decltype(bar(x)) { return; }; } struct A { }; void bar(A) { } int main() { f()(A{}); } but according to your answer, it would be equally ill-formed. Note the note "[ Note: The rules for name lookup in template definitions are described in [temp.res]. — end note ]" which means it actually intends for your quoted rule to not apply to the lambdas' use of bar, because it appears in a member function template's body, not a member function's body.Lawannalawbreaker
In fact, even if some rule would apply to a template definition and would have rendered it ill-formed: If the template has valid specializations, then no diagnostic must be generated and the program must be accepted according to "Otherwise, no diagnostic shall be issued for a template for which a valid specialization can be generated.".Lawannalawbreaker
G
7

foo(tag<int>{}); triggers the implicit instantiation of a specialization of the function call operator member function template of foo's closure type with the template argument tag<int>. This creates a point of instantiation for this member function template specialization. According to [temp.point]/1:

For a function template specialization, a member function template specialization, or a specialization for a member function or static data member of a class template, if the specialization is implicitly instantiated because it is referenced from within another template specialization and the context from which it is referenced depends on a template parameter, the point of instantiation of the specialization is the point of instantiation of the enclosing specialization. Otherwise, the point of instantiation for such a specialization immediately follows the namespace scope declaration or definition that refers to the specialization.

(emphasis mine)

So, the point of instantiation is immediately after main's definition, before the namespace-scope definition of bar.

Name lookup for bar used in decltype(bar(x)) proceeds according to [temp.dep.candidate]/1:

For a function call where the postfix-expression is a dependent name, the candidate functions are found using the usual lookup rules (6.4.1, 6.4.2) except that:

(1.1) — For the part of the lookup using unqualified name lookup (6.4.1), only function declarations from the template definition context are found.

(1.2) — For the part of the lookup using associated namespaces (6.4.2), only function declarations found in either the template definition context or the template instantiation context are found. [...]

Plain unqualified lookup in the definition context doesn't find anything. ADL in the definition context doesn't find anything either. ADL in the instantiation context, according to [temp.point]/7:

The instantiation context of an expression that depends on the template arguments is the set of declarations with external linkage declared prior to the point of instantiation of the template specialization in the same translation unit.

Again, nothing, because bar hasn't been declared at namespace scope yet.

So, the compilers are correct. Moreover, note [temp.point]/8:

A specialization for a function template, a member function template, or of a member function or static data member of a class template may have multiple points of instantiations within a translation unit, and in addition to the points of instantiation described above, for any such specialization that has a point of instantiation within the translation unit, the end of the translation unit is also considered a point of instantiation. A specialization for a class template has at most one point of instantiation within a translation unit. A specialization for any template may have points of instantiation in multiple translation units. If two different points of instantiation give a template specialization different meanings according to the one-definition rule (6.2), the program is ill-formed, no diagnostic required.

(emphasis mine)

and the second part of [temp.dep.candidate]/1:

[...] If the call would be ill-formed or would find a better match had the lookup within the associated namespaces considered all the function declarations with external linkage introduced in those namespaces in all translation units, not just considering those declarations found in the template definition and template instantiation contexts, then the program has undefined behavior.

So, ill-formed NDR or undefined behavior, take your pick.


Let's consider the example from your comment above:

template <class T>
struct tag { };

auto build() {
    auto foo = [](auto x) -> decltype(bar(x)) { return {}; };
    return foo;
}

tag<int> bar(tag<int>) { return {}; }

int main() {
    auto foo = build();
    foo(tag<int>{});
}

Lookup in the definition context still doesn't find anything, but the instantiation context is immediately after main's definition, so ADL in that context finds bar in the global namespace (associated with tag<int>) and the code compiles.


Let's also consider AndyG's example from his comment above:

template <class T>
struct tag { };

//namespace{
//tag<int> bar(tag<int>) { return {}; }
//}

auto build() {
    auto foo = [](auto x) -> decltype(bar(x)) { return {}; };
    return foo;
}

namespace{
tag<int> bar(tag<int>) { return {}; }
}

int main() {
    auto foo = build();
    foo(tag<int>{});
}

Again, the instantiation point is immediately after main's definition, so why isn't bar visible? An unnamed namespace definition introduces a using-directive for that namespace in its enclosing namespace (the global namespace in this case). This would make bar visible to plain unqualified lookup, but not to ADL according to [basic.lookup.argdep]/4:

When considering an associated namespace, the lookup is the same as the lookup performed when the associated namespace is used as a qualifier (6.4.3.2) except that:

(4.1) — Any using-directives in the associated namespace are ignored. [...]

Since only the ADL part of the lookup is performed in the instantiation context, bar in the unnamed namespace is not visible.

Commenting out the lower definition and uncommenting the upper one makes bar in the unnamed namespace visible to plain unqualified lookup in the definition context, so the code compiles.


Let's also consider the example from your other comment above:

template <class T>
struct tag { };

int main() {
    void bar(int);
    auto foo = [](auto x) -> decltype(bar(decltype(x){})) { return {}; };
    tag<int> bar(tag<int>);
    bar(tag<int>{});
    foo(tag<int>{});
}

tag<int> bar(tag<int>) { return {}; }

This is accepted by GCC, but rejected by Clang. While I was initially quite sure that this is a bug in GCC, the answer may actually not be so clear-cut.

The block-scope declaration void bar(int); disables ADL according to [basic.lookup.argdep]/3:

Let X be the lookup set produced by unqualified lookup (6.4.1) and let Y be the lookup set produced by argument dependent lookup (defined as follows). If X contains

(3.1) — a declaration of a class member, or

(3.2) — a block-scope function declaration that is not a using-declaration, or

(3.3) — a declaration that is neither a function nor a function template

then Y is empty. [...]

(emphasis mine)

Now, the question is whether this disables ADL in both the definition and instantiation contexts, or only in the definition context.

If we consider ADL disabled in both contexts, then:

  • The block-scope declaration, visible to plain unqualified lookup in the definition context, is the only one visible for all instantiations of the closure type's member function template specializations. Clang's error message, that there's no viable conversion to int, is correct and required - the two quotes above regarding ill-formed NDR and undefined behavior don't apply, since the instantiation context doesn't influence the result of name lookup in this case.
  • Even if we move bar's namespace-scope definition above main, the code still doesn't compile, for the same reason as above: plain unqualified lookup stops when it finds the block-scope declaration void bar(int); and ADL is not performed.

If we consider ADL disabled only in the definition context, then:

  • As far as the instantiation context is concerned, we're back to the first example; ADL still can't find the namespace-scope definition of bar. The two quotes above (ill-formed NDR and UB) do apply however, and so we can't blame a compiler for not issuing an error message.
  • Moving bar's namespace-scope definition above main makes the code well-formed.
  • This would also mean that ADL in the instantiation context is always performed for dependent names, unless we have somehow determined that the expression is not a function call (which usually involves the definition context...).

Looking at how [temp.dep.candidate]/1 is worded, it seems to say that plain unqualified lookup is performed only in the definition context as a first step, and then ADL is performed according to the rules in [basic.lookup.argdep] in both contexts as a second step. This would imply that the result of plain unqualified lookup influences this second step as a whole, which makes me lean towards the first option.

Also, an even stronger argument in favor of the first option is that performing ADL in the instantiation context when either [basic.lookup.argdep]/3.1 or 3.3 apply in the definition context doesn't seem to make sense.

Still... it may be worth asking about this one on std-discussion.


All quotes are from N4713, the current standard draft.

Giverin answered 4/2, 2018 at 21:29 Comment(1)
This definitly clears things up. I would probably never come up to this by the analysis of the standard by myself. I think I got the point, thank you!Caaba
A
5

From unqualified lookup rules ([basic.lookup.unqual]):

For the members of a class X, a name used in a member function body, [...], shall be declared in one of the following ways
— if X is a local class or is a nested class of a local class, before the definition of class X in a block enclosing the definition of class X

Your generic lambda is a local class within main, so to use bar, the name bar must appear in a declaration beforehand.

Anklebone answered 2/2, 2018 at 17:4 Comment(5)
I'm quite confused as following the paragraph I think this shouldn't also compile, should it?Caaba
@Caaba ADL ftw! Andy's cite means the name has to exist - it doesn't mean that that's the one that ends up being called.Murat
@Murat ahhh now everything make sense. Thanks!Caaba
I don't know whether this convinces me. For example this is not rejected auto f() { return [](auto x) -> decltype(bar(x)) { return; }; } struct A { }; void bar(A) { } int main() { f()(A{}); } but according to your answer, it would be equally ill-formed. Note the note "[ Note: The rules for name lookup in template definitions are described in [temp.res]. — end note ]" which means it actually intends for your quoted rule to not apply to the lambdas' use of bar, because it appears in a member function template's body, not a member function's body.Lawannalawbreaker
In fact, even if some rule would apply to a template definition and would have rendered it ill-formed: If the template has valid specializations, then no diagnostic must be generated and the program must be accepted according to "Otherwise, no diagnostic shall be issued for a template for which a valid specialization can be generated.".Lawannalawbreaker

© 2022 - 2024 — McMap. All rights reserved.