C++ had adopted the original
with message in C++11 by [N1720], the
attribute with message in C++14 by [N3760],
and the
attribute with message in C++20 by [P1301R4]. All of these introductions succeeded in introducing the ability to provide a user-defined message
accompanying the generated diagnostic (warning or error), thus helping to communicate the exact intent and reasoning of the library author and generate more friendly diagnostics.
This paper proposes to take a further step forward, and improve upon the other common and modern way of generating diagnostics, namely using
to delete a function,
by also introducing an optional message component.
1. Revision History
1.1. R1
-
Retargeted to C++26.
-
Rebase onto [N4958], which includes [P2361R6] so remove a note for it.
-
Since [P2741R3] had been adopted in Varna (2023-06), add a section for it.
-
Add previous poll result for [N4186].
-
In Kona (2023-11), EWGI sees D2573R1 and forwarded it to EWG with the following poll:
Poll: Given the committee’s limited time, EWGI believes P2573R1 is sufficiently developed and motivated to forward to EWG (and mark for LEWG).
SF | F | N | A | SA |
4 | 4 | 3 | 0 | 0 |
Outcome: Consensus 🎉
-
Add several notes on topics discussed in EWGI meeting.
1.2. R0
-
Initial revision.
2. Motivation
Introduced in C++11,
and
had joined
as possible alternative specification for a function body, instead of an ordinary brace-enclosed body of statements.
The original motivation for deleted function declaration via
is to replace (and supersede) the C++98/03-era common practice of declaring special member functions as
and not define them to disable their automatic generation. However,
’s addition had gained even greater power, for it is permitted to be used for any function, not just special members.
The original paper, [N2346], described the expected usage of deleted function as:
The primary power of this approach is twofold. First, use of default language facilities can be made an error by deleting the definition of functions that they require. Second, problematic conversions can be made an error by deleting the definition for the offending conversion (or overloaded function).
Looking back at present, ten years after the introduction of deleted functions, we can confidently conclude that
had become one of the key C++11 features that greatly
improved user experience on error usages of library functions and had been a success story of "Modern C++" revolution. We have seen relatively wide adoption both inside the standard library
and in the wider community scope, with over 40k results for
in the [ACTCD19] database. (Though, admittedly, the feature is still mostly used for member functions, especially its original motivation, disable SMFs.)
There are several reasons we preferred deleted functions over the traditional
-but-not-defined ones, including better semantics (
and other members are still unaccessible, turning a linker error into a compile-time error),
better diagnostics (instead of cryptic "inaccessible function" errors, the user directly know that the function is deleted), and greater power (not just SMFs).
As we are constantly striving to present a better and friendlier interface to the user of C++, this proposal wants to take the feature a step forward in the "better diagnostics" area:
Instead of an already friendlier but still somewhat cryptic "calling deleted function" error, we directly permit the library authors to present an optional extra message that
should be included in the error message, such that the user will know the exact reasoning of why the function is deleted, and in some cases, which replacement should the user heads to instead.
In other words, usage of
on a function by the library author practically means that
The library author is saying, "I know what you’re trying to do, and what you’re trying to do is wrong." (Source)
and after this proposal, my hope is that such usage will mean
The library author is saying, "I know what you’re trying to do, and what you’re trying to do is wrong. However, I can tell you why I think it is wrong, and I can point you to the right thing to do."
The proposed syntax for this feature is (arguably) the "obvious" choice: allow an optional string-literal to be passed as an argument clause to
.
Such a
clause will be usable whenever the original
clause is usable as a function-body.
Thus the usage will look like this:
void newapi (); void oldapi () = delete ( "This old API is outdated and already been removed. Please use newapi() instead." ); template < typename T > struct A { /* ... */ }; template < typename T > A < T > factory ( const T & ) { /* process lvalue */ } template < typename T > A < T > factory ( const T && ) = delete ( "Using rvalue to construct A may result in dangling reference" ); struct MoveOnly { // ... (with move members defaulted or defined) MoveOnly ( const MoveOnly & ) = delete ( "Copy-construction is expensive; please use move construction instead." ); MoveOnly & operator = ( const MoveOnly & ) = delete ( "Copy-assignment is expensive; please use move assignment instead." ); };
There is a tony table of how a non-copyable class evolves from C++98 to this proposal, thus introducing the benefits of the proposal and the user-friendliness it brings. All the examples (except the hypothetical one) are generated by x86-64 clang version 14.0.0 on Compiler Explorer. The usage client is
int main () { NonCopyable nc ; NonCopyable nc2 = nc ; ( void ) nc2 ; }
Standard | Code | Diagnostics / Comment |
C++98 |
|
|
Average user probably don’t know what " constructor" means. Also, /member usage still result in link-time error only.
| ||
C++11 (present) |
|
|
Great improvement: we can teach "deleted" means "usage is wrong" constantly, and everything is compile-time error. However, still for average user a bit hard to understand and don’t point out what to do instead. | ||
This proposal |
|
|
With minimal change, we get a huge boost in user/beginner friendliness, with ability to explain why and what to do instead. |
3. Usage Example
3.1. Standard Library
This part contains some concrete examples of how current deleted functions in the standard library can benefit from the new message parameter. All examples are based on [N4958].
Note: As will be discussed below, the standard does not mandate any diagnostic text for deleted functions; a vendor has the freedom (and is encouraged by this proposal) to implement these messages as non-breaking QoI features.
// [unique.ptr.single.general] namespace std { template < class T , class D = default_delete < T >> class unique_ptr { public : // ... // disable copy from lvalue unique_ptr ( const unique_ptr & ) = delete ( "unique_ptr<T> resembles unique ownership, so copy is not supported. Use move operations instead." ); unique_ptr & operator = ( const unique_ptr & ) = delete ( "unique_ptr<T> resembles unique ownership, so copy is not supported. Use move operations instead." ); } } // [memory.syn] namespace std { // ... template < class T > constexpr T * addressof ( T & r ) noexcept ; template < class T > const T * addressof ( const T && ) = delete ( "Cannot take address of rvalue." ); // ... template < class T , class ... Args > // T is not array constexpr unique_ptr < T > make_unique ( Args && ... args ); template < class T > // T is U[] constexpr unique_ptr < T > make_unique ( size_t n ); template < class T , class ... Args > // T is U[N] unspecified make_unique ( Args && ...) = delete ( "make_unique<U[N]>(...) is not supported; perhaps you mean make_unique<U[]>(N) instead?" ); } // [basic.string.general] namespace std { template < class charT , class traits = char_traits < charT > , class Allocator = allocator < charT >> class basic_string { public : // ... basic_string ( nullptr_t ) = delete ( "Construct a string from a null pointer is undefined behavior." ); }
3.2. Other
A hypothetical usage of new
that I can foresee to be commonly adopted is to mark the old API
first with
, and after a few versions, change it to
, so that further usage of old API directly results in an error message that
can explain the removal reason and also point towards the new API. Standard library vendors can also do this as a QoI feature through the freedom of zombie names.
4. Design
4.1. Previous Work
There have been three previous example of user-customisable error messages presented currently in the C++ standard:-
C++11
, by [N1720].static_assert ( expr , "message" ) -
C++14
, by [N3760].[[ deprecated ( "with reason" )]] -
C++20
, by [P1301R4].[[ nodiscard ( "with reason" )]]
This proposal naturally fits in the same category of providing reasons/friendly messages alongside the original diagnostics and can use a lot of the existing wordings.
A previous proposal, [N4186], proposed the exactly same thing, and is received favorably by EWG in Urbana (2014-11) (see [EWG152]):
Poll: Is this a problem worth solving?
SF F N A SA 13 5 1 3 2
However, there is no further progress on that proposal, and thus this proposal aims to continue the work in this direction.
4.2. Syntax and Semantics
The syntax I prefer is= delete ( "with reason" );
, in other words, an optional argument clause after the delete
keyword where the only argument allowed is a single string-literal.
The semantics will be entirely identical to regular
, which means that the new form can be exchanged freely with the old form, with the only difference being the diagnostic message.
Some additional semantics caveat is discussed in the Overriding Semantics section below.
4.3. Alternative Design and Issues
This section lists the alternative design choices and possible arguments against this proposal that have been considered.4.3.1. Alternative Syntax
A previous proposal [P1267R0] proposed a[[ reason_not_used ( "reason" )]]
attribute to enhance the compiler error in SFINAE failure cases.
While the motivation is similar, the problem it solved is slightly different as = delete
in general does not affect overload resolution.
In its "Future Directions" section, however, a series of [[ reason_xxx ]]
attributes were described, with the [[ reason_deleted ( "reason" )]]
described will have identical semantics with what this proposal proposes. This proposal proposes:
void fun () = delete ( "my reason" );
which in future-[P1267R0] world will be expressed as
[[ reason_deleted ( "my reason" )]] void fun () = delete ;
Personally, comparing these two syntaxes, I prefer the proposed
one as it is more concise, less noisy, has reasoning closer to the actual deletion and does not
have to repeat yourself by saying first, "I delete this function" then "the reason for delete is ..." twice. An advantage I can see for [P1267R0]-like solutions
is that if the committee decided to accept several
family (SFINAE,
,
, etc.), then the attribute with the same prefix can
be seen as more compatible with rest of the core language. However, since [P1267R0] is currently inactive, I will still propose the
syntax.
Of course, given that the function-body part of the grammar is relatively free, other (arcane?) syntax like
,
can be proposed;
I haven’t explored these syntaxes since I think the syntax I’m proposing is the most natural one and is fitting the existing practices.
4.3.2. Overriding Semantics
One of the new issues brought up by this new syntax is the overriding problem:struct A { virtual void fun () = delete ( "reason" ); }; struct B : public A { void fun () override = delete ( "different reason" ); };
Should overriding with a different (or no) message parameter be supported? (Notice that you cannot override deleted functions with non-deleted ones or vice versa, so this is the only form.)
I believe that this should be supported for the following reason:
-
Changing
message inside the library base classes should be non-breaking (non-observable).= delete -
Existing attributes allow this (and even went further, allow an overriding function to change from having attribute to not having).
4.3.3. Locales and Unevaluated Strings
I heard that there had been already some discussion in the committee for this proposal, probably in the discussion phase of [P1267R0]; the hesitancy is basically that the string-literals being accepted in the structure is unrestricted, thus may raise some doubt on how to handle things like= delete ( L"Wide reason" );
.
However, this is not a problem unique to this proposal. This problem also applies to the other three existing examples, and there is [P2361R6] to solve this problem in general. Therefore I do not see any reason locales of unevaluated strings should be an obstacle.
As for the problem of displaying text that cannot be represented in the execution character set, this paper follows the existing [P2246R1] changes to
wording and use "should" to allow flexibility.
4.3.4. Claim of Syntax Space
During EWGI review of this paper, some people expressed concern on the possibility of this proposal "claiming the syntax space" of possible further enhancement to= delete
. Namely, it is possible for a future proposal to alter = delete
the same way we altered explicit
in C++20, to give it an optional constant argument that
can express conditional = delete
. Thus, the following code may become a possibility in future expansion:
void foo () = delete ( sizeof ( int ) != 4 );
I think this argument is ungrounded for two reasons:
-
First of all, this proposal do not actually claim the full syntax space for the argument after
. Since in the C++ grammar, string-literals have more priority than ordinary expressions, it is still possible to add the ability to put an arbitrary constant boolean expression here. The semantics will simply be to prefer this proposal’s effect if a string literal is passed.= delete -
Secondly, I feel that
is not a viable future direction, since you can express essentially the same meaning with= delete ( < conditional - expression > )
today. Granted,requires
’s semantics are different from whatrequires
does, since the former SFINAE-away the function while the latter does not affect overload resolution at all. But I still find it hard to come up with a compelling use case for conditional delete that cannot be achieved by concepts.= delete
4.4. Proposal Scope
This proposal is a pure language extension proposal with no library changes involved and is a pure addition, so no breaking changes are involved. It is intended practice that exchanging= delete ;
for = delete ( "message" );
will have no breakage and no user-noticeable effect except for the diagnostic messages.
There has been sustained strong push back to the idea of mandating diagnostic texts in the standard library specifications, and in my opinion, such complaints are justified. This paper does not change this by only proposing to make such "deleted with reason" ability available and nothing more.
This paper encourages vendors to apply any text they see fit for the purpose as a QoI, non-breaking feature.
4.5. Future Extensions
There have been suggestions on supporting arbitrary constant-expressions instatic_assert
message parameter and other places so that we can generate something like
make_unique < int [ 5 ] > (...) is not supported ; perhaps you mean make_unique < int [] > ( 5 ) instead ?
for the invocation
(so that message come with concrete types). I do not oppose such extensions, and this proposal does not prevent supporting
in the future either.
During Varna (2023-06), [P2741R3] is adopted into the C++26 standard, giving
the ability to accept a user-generated string-like object as the message parameter.
However, I’m not sure if the corresponding support can be directly added to this feature, since
is a standalone statement, and this feature is currently an attachment to a function declaration,
which may lead to execution order issues. Therefore, currently I do not propose
as parts of this proposal.
I felt that the support for it should come together with a separate proposal to add user-generated message support to
and
too.
Some people suggested considering the same message parameter for other keywords in the language, such as
or
. While such constructs might be useful, I felt that this may quickly escalate to a general facility
that supports a custom message for every ill-formed program, and that is a step too far in my opinion.
is different from those two keywords in the sense that it might be used as a transitional method (to be added after the introduction of API to indicate deprecation),
while the other two constructs are usually set in stone after initial API. Furthermore, the applicable scope of
(only on classes and virtual member functions) and
(only on constructors) are both much more narrow than
(which can be applied on any function),
so I think just a controlled introduction of
should be most beneficial to users of the language without introducing too much novelty.
4.6. Target Vehicle
This proposal targets C++26.4.7. Feature Test Macro
The previous works all bump the relevant attributes or feature testing macros to a new value to resemble the change. However, C++11 default and deleted functions don’t have a feature test macro, so I propose to include a new language feature testing macro__cpp_deleted_function_with_reason
(name can be changed) with the usual value.
5. Implementation Experience
An experimental implementation of the proposed feature is located in my Clang fork at [clang-implementation], which is capable of handling
void foo () = delete ( "Reason" ); void fun () { foo ();}
and output the following error message: (close to what I want; of course, there are other formats such as put reason on separate lines)
propose . cpp : 2 : 13 : error : call to deleted function 'foo ': "Reason" void fun () { foo ();} ^~~ propose . cpp : 1 : 6 : note : candidate function has been explicitly deleted void foo () = delete ( "Reason" ); ^ 1 error generated .
The implementation is very incomplete (no support for constructors, no feature-test macro, only a few testing, etc.), so it is only aimed at proving that the vendors can support the proposed feature relatively easily.
6. Wording
The wording below is based on [N4958].
Wording notes for CWG and editor:
-
This wording factors existing
in the function-body out to a new grammar token, deleted-function-body, to allow reusing. An alternative approach is to change [dcl.constexpr] to use the "deleted function" term directly.= delete ; -
The current wording is basically a combination of
([dcl.pre]/10, for error instead of warning) andstatic_assert
([dcl.attr.nodiscard]).[[ nodiscard ]]
6.1. 9.5 Function definitions [dcl.fct.def]
6.1.1. 9.5.1 In general [dcl.fct.def.general]
Function definitions have the form
function-definition: attribute-specifier-seqopt decl-specifier-seqopt declarator virt-specifier-seqopt function-body attribute-specifier-seqopt decl-specifier-seqopt declarator requires-clause function-body function-body: ctor-initializeropt compound-statement function-try-block = default ;= delete ;deleted-function-body deleted-function-body: = delete ; = delete ( unevaluated-string ) ;
6.1.2. 9.5.3 Deleted definitions [dcl.fct.def.delete]
Clause 1:A deleted definition of a function is a function definition whose function-body is of the form
deleted-function-body or an explicitly-defaulted definition of the function where the function is defined as deleted. A deleted function is a function with a deleted definition or a function that is implicitly defined as deleted.
= delete ;
Clause 2:
A program that refers to a deleted function implicitly or explicitly, other than to declare it, is ill-formed
., and the resulting diagnostic message ([intro.compliance]) should include the text of the unevaluated-string, if one is supplied.
[Note 1: This includes calling the function implicitly or explicitly and forming a pointer or pointer-to-member to the function. It applies even for references in expressions that are not potentially-evaluated. For an overload set, only the function selected by overload resolution is referenced. The implicit odr-use ([basic.def.odr]) of a virtual function does not, by itself, constitute a reference. The unevaluated-string, if present, can be used to explain the rationale for why the function is deleted and/or to suggest a replacing entity. — end note]
6.2. 15.11 Predefined macro names [cpp.predefined]
In [tab:cpp.predefined.ft], insert a new row in a place that respects the current alphabetical order of the table, and substituting20 XXYYL
by the date of adoption.
Macro name | Value |
|
|