| Issue |
165026
|
| Summary |
[clang:frontend][libcxx][feature request] Introduce C++20 concept naming attribute to improve readability of template constraint failure errors
|
| Labels |
clang,
libc++
|
| Assignees |
|
| Reporter |
void2012
|
The C++20 standard introduces the `concept` system which is designed to improve readability, source code size, and compilation time, as opposed to the traditional SFINAE techniques such as `std::enable_if`. The `concept` system comes with many benefits, however, the template constraint failures still may puzzle developers, especially if the template nesting is deep and there are many constraints on different function/struct/etc. arguments/etc.. Here is my proposal to the clang frontend to address this issue using what I call `concept` naming, implemented via introducing the new clang compiler-specific attribute for concepts.
Consider the following code:
```cpp
// Example1.cpp
template<typename T>
concept integral_concept = __is_integral(T);
template<integral_concept T>
int consume(T)
{ return 1; }
int test = consume(int{});
```
This snippet compiles because the type `int` satisfies the `integral_concept` concept, no error expected.
Now, if we add the structure(it may be any structure or non-integral, even `void`, but for the sake of example we do it with structure) and pass argument of said structure type to the `consume` template function, we are supposed to see the error.
```cpp
// Example2.cpp
template<typename T>
concept integral_concept = __is_integral(T);
struct NonIntegral {};
template<integral_concept T>
int consume(T)
{ return 1; }
int test1 = consume(int{});
int test2 = consume(NonIntegral{});
```
The error reported is as follows:
```
<source>:11:13: error: no matching function for call to 'consume'
11 | int test2 = consume(NonIntegral{});
| ^~~~~~~
<source>:7:5: note: candidate template ignored: constraints not satisfied [with T = NonIntegral]
7 | int consume(T)
| ^
<source>:6:10: note: because 'NonIntegral' does not satisfy 'integral'
6 | template<integral_concept T>
| ^
<source>:2:20: note: because '__is_integral(NonIntegral)' evaluated to false
2 | concept integral_concept = __is_integral(T);
| ^
```
As the template nesting level grows and the complexity of a template increases, the error message may become a mess. I have an idea how to make them readable. This is done via a compiler-specific attribute, which can be applied to concepts. Syntax:
`[[clang::concept_desc(format...)]]`, where `format` may be either `printf`-like format, or `std::format`-like format or anything that will be used to print a more readable error message.
Example:
```cpp
template<typename T>
[[clang::concept_desc("integral")]]
concept integral_concept = __is_integral(T);
```
Now, the error message may look like this(see Example2.cpp code snippet):
```
<source>:11:13: error: no matching function for call to 'consume'
11 | int test2 = consume(NonIntegral{});
| ^~~~~~~
<source>:7:5: note: function argument must be `integral`: `NonIntegral` does not satisfy `integral` (aka 'std::integral') [with T = NonIntegral]
7 | int consume(T)
| ^
```
The key idea here is that the developer doesn't always need to know how the constraint was implemented(the underlying details, including the implementation, may be printed later, but the very first message should explicitly say what exactly went wrong), he just needs to know what caused the constraint failure imposed by the concept(s).
Suppose we have a function that takes more arguments with two different concept constraints
```cpp
template<std::integral T, std::floating_point F>
int foo(T iarg, F farg);
```
OR
```cpp
int foo(std::integral auto iarg, std::floating_point auto farg);
```
Now let's pass some different arguments to `foo`
```
int test1 = foo(1, 1.0f); // OK
int test2 = foo(std::string{}, 1.0f); // ERROR, see below
int test3 = foo(std::string{}, std::list<int>{}); // ERROR, see below
```
Error printed for `test2` case may look something like this:
```
<source>:9:13: error: no matching function for call to 'foo'
9 | int test2 = foo(std::string{}, 1.0f);
| ^~~
<source>:6:5: note: 1st function argument must be 'integral', `std::string` (aka 'basic_string<char>') does not satisfy constraint `integral` (aka `std::integral`): [with T = std::string, F = float]
6 | int foo(T iarg, F farg);
| ^
<source>:5:10: note: because 'std::string' (aka 'basic_string<char>') does not satisfy 'integral'
5 | template<std::integral T, std::floating_point F>
| ^
```
For `test3`:
```
<source>:10:13: error: no matching function for call to 'foo'
10 | int test3 = foo(std::string{}, std::list<int>{});
| ^~~
<source>:6:5: note: 1st argument must be 'integral', 2nd argument must be 'floating point', `std::string` (aka 'basic_string<char>') does not satisfy `integral` (aka `std::integral`), `std::list<int>` does not satisfy `floating point` (aka `std::floating_point`) [with T = std::string, F = std::list<int>]
6 | int foo(T iarg, F farg);
| ^
```
Now let's see how this could work with concepts that take any number of template arguments:
```cpp
template< class R, class F, class... Args >
[[clang::concept_desc("function which returns {} and has arguments of type {...}", R, Args...)]]
concept invocable_r = std::is_invocable_r_v<R, F, Args...>;
```
And if we pass function of type `int(std::string, bool)` to the function which takes invocable argument with constraint `invocable_r<float, std::string, int>`, the error printed would say that the argument `does not satisfy 'function which returns 'float', and has arguments of type 'std::string' (aka 'std::basic_string<char>'), and 'int'', but provided argument 'Func' has return type 'int' ('float' expected) and arguments of type 'std::string' (aka 'std::basic_string<char>') and 'bool'('int' expected)`
This idea may be extended beyond the function templates, as this is just a presentation of the general idea. I hope this gets some attention from clang developers. Thank you!
_______________________________________________
llvm-bugs mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-bugs