Search code examples
c++language-lawyerc++20c++-conceptsoverload-resolution

Compiler diverge on overload resolution using argument-dependent lookup and constraint expression


In the following program template function f calls overloaded template function p and one of the overloads is declared after f, but as far as I understand it must be found at the point of instantiation using argument-dependent lookup:

struct A{};

constexpr bool p(auto) { return false; }
constexpr bool f(auto v) { return p(v); }
constexpr bool g() { return f(A()); }
constexpr bool p(auto) requires true { return true; }

static_assert( f(A{}) );

The static_assert passes in MSVC and in GCC only with -O0 option, and it fails in Clang and in GCC with -O1 and higher optimization options. Online demo: https://godbolt.org/z/crvE5azWe

Is there any undefined behavior in the program, and if not, which compiler is correct here?


Solution

  • The declarations of the p templates are IFNDR (ill-formed, no diagnostic required) per https://eel.is/c++draft/temp#over.link-7.sentence-2:

    The two declarations have corresponding signatures (which doesn't consider trailing requires-clauses), but they do not correspond because that does require equivalent trailing requires-clauses. If they did correspond however, then the declarations would (re-)declare the same function template.

    Also, the two function templates accept and satisfy the same template argument lists. requires true does not constrain the template in any form.

    Two function template declaration can't be "functionally equivalent" but not "equivalent" (in which case they would declare the same function template). If the templates have the exact same signature and accept/satisfy the same template arguments, then they should be the same template and to make sure that they can be recognized as same template, their declarations should have the exact same form (except for template parameter names).


    Even if you resolve that issue by making the second overload not functionally equivalent, you still have the following problem:

    g is not a template, so the use of f(A()) in its definition already causes instantiation of the specialization f<A>. Since this is earlier than the instantiation of the same specialization that would be required from static_assert( f(A{}) ); the latter doesn't matter.

    There are two points of instantiation for a function template specialization: Either directly after the declaration from which the instantiation is required, or at the end of the translation unit.

    At the first point of instantiation the second p overload hasn't been declared yet. Therefore it can't be found by name lookup. At the second point of instantiation the second p has been declared and, as you are correctly arguing, should be found by ADL and used for the call.

    So, the overload resolution gives different results depending on the point of instantiation. If two points of instantiation of a template specialization cause the meaning of the program to change, then the program is IFNDR as well.

    In other words, you are not allowed to add an overload that would have been chosen by overload resolution via ADL after the point where the overload resolution is first required. All functions that should be connected to the class via ADL should be declared before any use of the class (typically in a header for the class).