Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 872
W.F. Avatar asked Feb 02 '18 15:02

W.F.


People also ask

What is argument-dependent lookup why it is useful?

The compiler can use argument-dependent name lookup to find the definition of an unqualified function call. Argument-dependent name lookup is also called Koenig lookup. The type of every argument in a function call is defined within a hierarchy of namespaces, classes, structures, unions, or templates.

How does ADL work C++?

In the C++ programming language, argument-dependent lookup (ADL), or argument-dependent name lookup, applies to the lookup of an unqualified function name depending on the types of the arguments given to the function call.

What is unqualified call c++?

A function call expression such as func(a,b,c) , in which the function is named without the :: scope operator, is called unqualified.


1 Answers

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.

like image 58
bogdan Avatar answered Sep 29 '22 12:09

bogdan