-
Couldn't load subscription status.
- Fork 15k
Description
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:
// 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.
// 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:
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
template<std::integral T, std::floating_point F>
int foo(T iarg, F farg);OR
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:
template<class F, class R, 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!