Field | Value |
---|---|
DIP: | 1038 |
Review Count: | 2 |
Author: | Paul Backus (snarwin@gmail.com) |
Implementation: | dlang/dmd#13589 |
Status: | Accepted |
This DIP proposes a new attribute, @mustuse
, which can be applied to a
struct
or union
type to make ignoring an expression of that type into a
compile-time error. It can be used to implement alternative error-handling
mechanisms for code that cannot use exceptions, including @nogc
and BetterC
code.
- Rationale
- Prior Work
- Description
- Breaking Changes and Deprecations
- Reference
- Copyright & License
- Reviews
Currently in D, the only generally applicable way for a function to send a signal to its caller that the caller cannot ignore is to throw an exception. For a variety of reasons, however, the use of exceptions is not always possible or desirable. Examples of code that may want or need to avoid exceptions include:
- code that is written in a language other than D (for example, C or C++);
- code written in D that may be called from another language;
- code that does not want to depend on the D runtime;
- code that cannot afford the run-time performance overhead of exceptions.
Since D is intended to be a systems language suitable for writing low-level, high-performance code with seamless C and C++ interoperability, its feature set should support reliable error handling in all of these use-cases.
@mustuse
helps D achieve this goal by enabling reliable error handling both
for functions that use error codes and for functions that use algebraic
"result" types to signal failure to their callers.
One possible alternative to exceptions, proposed by Vladimir
Panteleev, is for a function to return an error code wrapped in a
struct
that assert
s (or throw
s) in its destructor at run time if it has
not been used (where "using" means calling a method to retrieve the wrapped
value).
While this addresses some of the use-cases above, it has one major shortcoming
compared to @mustuse
: it reports ignored errors at run time rather than
compile time.
Another alternative is for a function to return error information via an out
parameter. Since a call to the function will not compile with a missing
argument, the calling code is forced at compile time to visibly acknowledge the
possibility of an error.
Unfortunately, using out
parameters for error handling is not a generally
applicable solution because the programmer is not always free to change a
function's argument list to include an out
parameter. Reasons for this
include:
- the function's argument list is part of an established public API, and changing it would break other code;
- the function is used as a callback by another function that requires it to accept a specific list of arguments;
- the function is an operator overload.
By contrast, @mustuse
requires only the freedom to change a function's return
type, and this change can in many cases be made backwards-compatible by using
alias this
to allow implicit conversion of the new return type to the old
one.
Some functions have side effects but are nevertheless unlikely to be called for those side effects alone. Examples of such functions include:
- functions that acquire resources, such as
malloc
andmmap
; - functions that generate random numbers, such as
rand
anduniform
; - generic functions that may or may not cause side effects depending on their
arguments, such as
filter
andmap
.
What these functions have in common is that their side effects, if any, are considered implementation details, rather than being part of their documented behavior. As a result, calling code cannot rely on them to cause any specific side effects without risking breakage if and when those implementation details change.
Though ignoring the return values of these functions is unlikely to result in
disaster, it is still a probable programming mistake which @mustuse
could
help guard against.
This DIP does not recommend changing the return types of any existing functions
in Phobos or the D runtime to @mustuse
types, since doing so would constitute
a breaking API change. However, authors of new code would still benefit from
having @mustuse
in the language and existing projects (including Phobos and
the D runtime) could adopt @mustuse
on a case-by-case basis if the benefit
were judged to be worth the potential for breakage.
The D compiler already warns about discarding the value of an expression if it
has no side effects, including the case where the expression is a call to a
strongly pure
and nothrow
function. An attribute that would allow the
programmer to extend this warning by marking specific functions or types as
non-discardable has been proposed several times on the D issue tracker and
forums; see the Reference section below for details.
- C++17's
[[nodiscard]]
attribute. - Rust's
#[must_use]
attribute. - GCC's
warn_unused_result
attribute. - Clang's
warn_unused_result
attribute.
Language | Attribute | Applies to | Diagnostic |
---|---|---|---|
C++17 | [[nodiscard]] |
Types and functions | Warning |
Rust | #[must_use] |
Types and functions | Warning |
C (GCC, Clang) | warn_unused_result |
Functions | Warning |
D (DIP 1038) | @mustUse |
Types | Error |
@mustUse
is a compiler-recognized user-defined attribute declared in the D
runtime module core.attribute
. It takes no arguments.
An expression is considered to be discarded if and only if either of the following is true:
- it is the top-level Expression in an ExpressionStatement, or
- it is the AssignExpression on the left-hand side of the comma in a CommaExpression.
It is a compile-time error to discard an expression if all of the following are true:
- it is not an assignment expression, an increment expression, or a decrement expression; and
- its type is a
struct
orunion
type whose declaration is annotated with@mustUse
.
An assignment expression is either a simple assignment expression or an assignment operator expression.
An increment expression is a UnaryExpression or PostfixExpression whose
operator is ++
.
A decrement expression is a UnaryExpression or PostfixExpression whose
operator is --
.
It is a compile-time error to annotate any function declaration or aggregate
declaration other than a struct
or union
declaration with the @mustUse
attribute. The purpose of this rule is to reserve such usage for possible
future expansion.
An error resulting from @mustUse
can be suppressed by prepending cast(void)
to the offending expression, since void
is not a struct
or union
type
annotated with @mustUse
. The error message for discarding an expression of a
@mustUse
type should suggest using cast(void)
if the programmer intended to
discard the expression.
The design for @mustUse
described above was chosen to achieve the best
possible balance among the following goals:
-
Simplicity of specification. A language feature that is difficult to specify precisely is likely to also be difficult to learn and difficult to use correctly.
-
Simplicity of implementation. A language feature that has a complex implementation is likely to suffer from implementation bugs. A complex implementation also increases the burden on compiler maintainers, which makes future improvements to both the language and its compilers more difficult to achieve.
-
Rigor. A language feature that provides strong guarantees that programmers can rely on is more useful that one that provides weak guarantees or permits exceptions and special cases. Rigorous language features compose more easily than non-rigorous ones.
A few possible alternative designs, along with the reasons for their rejection, are discussed below.
As described above, @mustUse
is restricted to use with user-defined struct
and union
types, and cannot easily be applied to functions that return other
kinds of types. One way to address this shortcoming would be to allow
@mustUse
to be used as a function attribute. For example:
// Hypothetical usage
@mustUse int dontIgnoreMe() { return 42; }
void main()
{
// error: cannot discard return value of @mustUse function `dontIgnoreMe`
dontIgnoreMe();
}
The main challenge for this design is deciding how to deal with statements that
discard the return value of a @mustUse
function indirectly. For example:
a ? b : dontIgnoreMe();
(writeln("side effect"), dontIgnoreMe());
(() => dontIgnoreMe())();
There are two possibilities:
- Error. This requires all discarded expressions to be searched,
recursively, for calls to
@mustUse
functions in positions where their return values might be discarded. - No error. This allows return values of
@mustUse
functions to be accidentally discarded in some cases, without an explicit cast or type conversion.
Option (1) sacrificies specification simplicity and implementation simplicity to maintain rigor; option (2) sacrifices rigor to maintain simplicity.
Rather than make either sacrifice, this DIP proposes a design that allows both
rigor and simplicity to be maintained, and reserves the possibility for a
future DIP to allow @mustUse
as a function attribute.
As described above, @mustUse
cannot be applied to class
or interface
types. Allowing this usage would make @mustUse
more widely applicable.
The main challenge for this design is deciding how to deal with @mustUse
subclasses. For example:
class Parent {}
// Hypothetical usage
@mustUse class Child : Parent {}
Parent fun() { return new Child(); }
void main()
{
fun();
}
Again, there are two possibilities:
- Error. This requires that if a child class is annotated with
@mustUse
, its parent class must also be annotated with@mustUse
. - No error. This allows values of
@mustUse
class types to be accidentally discarded in some cases, without an explicit cast or type conversion.
Because every D class inherits from Object
and Object
is not annotated with
@mustUse
, choosing option (1) would mean restricting @mustUse
to
interface
types and non-D class
types (i.e., extern(C++)
,
extern(Objective-C)
, and COM classes). It is uncertain whether such a
restricted version of @mustUse
would be worth the complexity cost of
specifying and implementing it.
Conversely, option (2) is simpler to specify and implement, but the guarantee it provides is so weak as to make it nearly useless.
Since both options are unattractive, this DIP chooses neither, but reserves the
possibility for a future DIP to allow @mustUse
as an attribute for classes
and interfaces.
No breaking changes or deprecations are anticipated.
- Issue 3882 - Unused result of pure functions
- Issue 5464 - Attribute to not ignore function result
- Issue 20165 - Add standard @nodiscard attribute for functions
- xxxInPlace or xxxCopy? (D.General)
- There is anything like nodiscard attribute in D? (D.Learn)
- Idiomatic way to express errors without resorting to exceptions (D.Learn)
- Vladimir Panteleev's
Success
type.
Copyright (c) 2020 - 2022 by the D Language Foundation
Licensed under Creative Commons Zero 1.0
The following points were raised in the feedback thread:
- How will the cast semantics interact with
@safe
? The DIP author responded thatcast(void)
is always@safe
. @nodiscard
should not apply tovoid
functions. The DIP author agreed but noted that it's not transitive and can never be inferred, so the solution is simply for programmers not to apply it tovoid
functions. The equivalent features in C++ and Rust do not make this exception.- Regarding the "Error handling without exceptions" subsection of the Rationale:
- It's possible to build a D interface that throws exceptions when binding to other languages
- The commenter is aware of at least one package marked "optimized for fast execution" that throws
- The numbers are inaccurate. The DIP author is aware of this, but the effort required for complete accuracy is prohibitive, and he believes the proposal is stronger even with inaccurate numbers than without.
- The DIP doesn't mention constructors. The DIP author replied that the rules cover this implicitly since constructors are functions.
- The DIP should mention if
@nodiscard
applies when the attribute is attached to a type and a constructor is called for that type. The DIP author replied that a call to a type's constructor is an expression of that type and, therefore, when@nodiscard
is applied to the type, the expression is non-discardable. - The DIP does not cover template functions with auto-inferred types, the return type of which can be
void
or not depending upon the template arguments. The DIP author replied he could not imagine a need to mark such a function as@nodiscard
.
There were only two actionable items of feedback in this round. One was about an ambiguity in the text. The other:
Given a function @nodiscard int foo
called in, e.g., a ternary operation a ? b : foo()
, there is no error raised about the return value of foo
being ignored. The reviewer sees this as a critical shortcoming that should be addressed. The DIP author responded that this behavior is identical to that of GCC, C++17, and Rust. He explained that such error cases could be detected if the annotation were a type qualifier, e.g., @nodiscard(int) foo
, but that can lead to undesirable errors. He suggested that syntax-level checks could be removed from the proposal if they are deemed inadequate.
The language maintainers accepted this DIP with a request for changes:
- rename
@noDiscard
, as they want to avoid adding addional negative attributes to the language. - address issues that arise from the feature's interaction with inheritance when applied to classes.
- develop rules for handling covariance and contravariance when applied to functions.
The DIP author addressed these requests by renaming the attribute to @mustuse
and allowing it only on structs and unions. His rationale for the latter is described in the section, Design Goals and Possible Alternatives.
The maintainers approved the author's changes and accepted the revised version of the DIP.