llvm / llvm-project

The LLVM Project is a collection of modular and reusable compiler and toolchain technologies.
http://llvm.org
Other
28.22k stars 11.64k forks source link

[clang] value parameter const-qualification influences overload resolution #82882

Open h-2 opened 7 months ago

h-2 commented 7 months ago
#include <concepts>

template <typename T>
void foobar(T) {}

template <std::integral T>
void foobar(T const) {}

int main()
{
    foobar(3);
}

Expected behaviour:

This code is well-formed; the second overload is chosen (because more constrained); the const is irrelevant.

GCC and MSVC show this behaviour.

Actual behaviour:

Clang rejects this code:

<source>:11:5: error: call to 'foobar' is ambiguous
   11 |     foobar(3);
      |     ^~~~~~
<source>:4:6: note: candidate function [with T = int]
    4 | void foobar(T) {}
      |      ^
<source>:7:6: note: candidate function [with T = int]
    7 | void foobar(T const) {}
      |      ^
1 error generated.
Compiler returned: 1
llvmbot commented 7 months ago

@llvm/issue-subscribers-clang-frontend

Author: Hannes Hauswedell (h-2)

```cpp #include <concepts> template <typename T> void foobar(T) {} template <std::integral T> void foobar(T const) {} int main() { foobar(3); } ``` **Expected behaviour:** This code is well-formed; the second overload is chosen (because more constrained); the `const` is irrelevant. GCC and MSVC show this behaviour. **Actual behaviour:** Clang rejects this code: ``` <source>:11:5: error: call to 'foobar' is ambiguous 11 | foobar(3); | ^~~~~~ <source>:4:6: note: candidate function [with T = int] 4 | void foobar(T) {} | ^ <source>:7:6: note: candidate function [with T = int] 7 | void foobar(T const) {} | ^ 1 error generated. Compiler returned: 1 ```
zygoloid commented 7 months ago

I think clang is correct here. The cv-qualification in this case is part of the signature (if T is an array type, the two templates don't have the same parameter type after decay), so the templates don't have the same parameter types and so aren't ordered by their constraints.

h-2 commented 7 months ago

if T is an array type, the two templates don't have the same parameter type after decay), so the templates don't have the same parameter types and so aren't ordered by their constraints.

Can you elaborate?

I am seeing this with Clang:

#include <concepts>

template <typename T>
void foobar(T) {}

template <std::regular T>
void foobar(T const) {}

int main()
{
    using Arr = int[3];
    foobar(Arr{1,2,3});
}

emits:

<source>:12:5: error: call to 'foobar' is ambiguous
   12 |     foobar(Arr{1,2,3});
      |     ^~~~~~
<source>:4:6: note: candidate function [with T = int *]
    4 | void foobar(T) {}
      |      ^
<source>:7:6: note: candidate function [with T = int *]
    7 | void foobar(T const) {}
      |      ^
1 error generated.
Compiler returned: 1

Which indicates that the template parameters are the same, or not?

zygoloid commented 7 months ago

Here's an example:

template <typename T>
void foobar(T) { std::puts("non-const"); }

template <typename T> concept C = true;

template <C T>
void foobar(T const) { std::puts("const"); }

int main()
{
    const int *p;
    int *q;
    foobar<int[1]>(p);
    foobar<int[1]>(q);
}

[temp.func.order]/6.2.2 is the relevant rule:

Otherwise, if the corresponding template-parameters of the template-parameter-lists are not equivalent ([temp.over.link]) or if the function parameters that positionally correspond between the two templates are not of the same type, neither template is more specialized than the other.

Per [dcl.fct]/5, the type of the function parameter includes the top-level cv-qualifiers; those are removed when forming the function type, but retained in the parameter types. (CWG1001 is also relevant here, but in this case the wording is already clear.)

zygoloid commented 7 months ago

I've emailed CWG to check that this is the intended behavior.

h-2 commented 7 months ago

Here's an example:

Ah, this is interesting! I haven't passed around raw arrays in many years, so I didn't remember that rightmost const is pulled into the decayed pointer type. So you have made a case for when this distinction could be useful!

However, don't you agree that it is unexpected then that the concept is required at all? Since it isn't the thing that decides which template gets picked, correct? It's indeed the const, so shouldn't this then work without the constraints? [spoiler: it does not]

Fun fact: GCC chokes on your example, because it creates the same mangled name for both templates.

I've emailed CWG to check that this is the intended behavior.

Cool, let's see what they say. I am not subscribed to the core reflector, so please let me know what comes up 🙂

frederick-vs-ja commented 7 months ago
#include <concepts>

template <typename T>
void foobar(T) {}

template <std::integral T>
void foobar(T const) {}

int main()
{
    foobar(3);
}

The original case was fixed in #81449 (Godbolt link).

#include <concepts>

template <typename T>
void foobar(T) {}

template <std::regular T>
void foobar(T const) {}

int main()
{
    using Arr = int[3];
    foobar(Arr{1,2,3});
}

This is also fixed (Godbolt link).

I think Clang is correct now - the top-level cv-qualifiers are dropped after determining the actual function type.

zygoloid commented 7 months ago

@frederick-vs-ja Per the discussion in this issue, I think clang was correct before. But let's wait and see what CWG says before reverting #81449.