Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make a concept fail with a custom error message (C++20)

Concepts are great at pinpointing an error to the line of code where "a constraint was not satisfied".

I'm wondering, however, if it is possible to issue a custom informational message there. The upside of static_assert was exactly that possibility. Use case: Any library that wants to help the user in figuring out why a certain expression was not satisfying the constraints.

Here is a simple example, just to have some code. You might argue that any half-decent 'user' must be able to figure out the compiler's note "because 'is_base_of<Base, C>' evaluated to false", but more custom information could not hurt. There will be more complicated concepts for sure.


template<typename B, typename D> 
concept is_base_of = std::is_base_of_v<B, D>;

template <typename T, is_base_of<T> BaseType>
struct BaseWrapper { };  

int main() 
{
    class Base {};
    class Derived : public Base {};
    class C {};

    using T1 = BaseWrapper<Derived,Base>;
    using T2 = BaseWrapper<C,Base>;         // fails right here, but a custom message would be nice
}

like image 702
non-user38741 Avatar asked Aug 25 '20 12:08

non-user38741


2 Answers

[EWG] P1267R0: Custom Constraint Diagnostics

I'm wondering, however, if it is possible to issue a custom informational message there.

There is currently no such native feature as part of Concepts, but there was a WG21/SD-1 paper that covered specifically this topic:

  • P1267R0: Custom Constraint Diagnostics

Design

There is already precedent in the standard for custom diagnostics:

[[deprecated("reason")]]

static_assert(cond, reason)

We propose adding a new attribute, similar to ​[[deprecated("reason")]]​, for custom constraint diagnostic messages. Let’s call this new attribute[[reason_not_used("reason")]]​ for now:

template​ <fixed_string Pattern>  
  requires Correct_Regex_Syntax<Pattern>  
  [[reason_not_used("invalid regex syntax")]]
bool​ match(string_view sv);

When this attribute is placed on a function, the diagnostic message would be used when:

  • The function was considered and rejected as a candidate for a function call, for any reason (deduction/substitution failure, requires clause constraint failure, no suitable conversion, etc).
  • The function call found no matching overload and thus lead to a compilation failure.

Today, when a function call fails to find a match, C++ compilers typically print out a list of all the candidates considered. We envision this new attribute as being additive to existing diagnostics, in the same way that static_assert​’s diagnostic message is.

[...]

Future Directions

This attribute could also potentially be used on class and alias templates to provide custom diagnostic messages for constraint failures and when selecting a specialization (in the case of class templates):

template​ <​typename​ T>  
  requires FloatingPoint<T> || Integral<T>  
  [[reason_not_used("the element type must be numeric")]]
struct​ matrix {};

matrix<string> a;

#:#:​error: ​template constraint failure 
matrix<string​>​ a;​
             ^
#:#:​note: ​constraints not satisfied: ​the element type must be numeric
#:#:​note: ​within ...

This attribute could also be attached to concept definitions, and used whenever checking that concept’s constraints fails.

Its audience was the Evolution Working Group (EWG), and according to @DavisHerring (C++ committee member), albeit not published externally, it was rejected:

@DavisHerring:

[...] that paper was considered in 2018 and “rejected”—that is, new information (like “It’s 2021 and in practice concept error messages are still bad”) would be needed to consider it again.

Recently, papers have been given very brief public statuses; for older papers like this one, the references are unfortunately internal.

However, most likely inspired by this StackOverflow Q&A, the topic has resuscitated on r/cpp, as the thread Custom 'diagnostics' for concepts, which discusses an alternative, simpler approach to custom diagnostics for concepts [emphasis mine]

Custom 'diagnostics' for concepts

I'd like to 'propose' the following simple attribute for concepts:

template <typename T> [[requirement_failed("reason")]]
concept MyConcept = /*some expression*/;

template<typename T>
concept MyConcept2 = requires(T t) {    
    /*some expression*/;
} [[requirement_failed("reason")]];

The purpose should be quite obvious: "reason" is issued as a compiler note, when the requirements are not met.

Notes:

There is P1267R0, but I think it's just not put or presented it in a simple enough way. Allowing the attribute on any definition that 'requires' will likely result in a mess.

Allowing it on concept definitions only will keep things clean. It avoids duplication for various functions making use of the same concept. It avoids over-use of the feature, as there should not be an excessive amount of concepts.

In my view, concepts that are part of an interface should offer 'diagnostic' messages as a best practice.

I don't think I have the guts, insight or time to make this into an official proposal, even if it is a very simple thing. So I thought, I'd just mention it here. It's also hard to imagine that something similar has not been considered yet, by some concepts committee.

Anyway, I wanted to draw some attention to it.

The author explicitly mentions he/she would not like to go forward with a formal paper on the topic. If e.g. the OP of this SO Q&A sees this as an important feature, one approach would be to pick up and (re-)formalize this topic into a paper.

like image 178
dfrib Avatar answered Oct 30 '22 22:10

dfrib


I think the best you can do right now, without the kind of new language feature that dfri points out, is just to write your own static assert.

In this case, to avoid repeating yourself, you can add a concept to check if you can instantiate a template:

template <template <typename...> class Z,
    typename... Args>
concept can_instantiate = requires {
    typename Z<Args...>;
};

and then assert that you can create that alias first:

static_assert(can_instantiate<BaseWrapper, C, Base>, "some message here");
using T2 = BaseWrapper<C,Base>;

Alternatively, you can move BaseWrapper's constraints themselves to be static assertions:

template <typename T, typename BaseType>
struct BaseWrapper {
    static_assert(is_base_of<BaseType, T>, "some message here");
};  

This has the drawback of no longer being externally detectable - which may or may not be important in practice.

like image 2
Barry Avatar answered Oct 30 '22 23:10

Barry