LEWG [[nodiscard]] policy
Document number: P3162R0
Date: 2024-02-22
Authors: Darius Neațu <dariusn@adobe.com>, David Sankel <dsankel@adobe.com>
Audience: Library Evolution
Abstract
The committee spends substantial time deciding whether or not [[nodiscard]] is appropriate for newly introduced standard functions, and the decisions made are often incoherent. We propose a policy that results in minimal syntactic overhead while retaining the detection of the most egregious defects.
Examples illustrating proposed policy |
[[nodiscard]] bool map<T>::empty() const noexcept;
[[nodiscard]] void* operator new(size_t);
template<class T, size_t N>
constexpr bool operator==(const array<T, N>& x, const array<T, N>& y);
template<class T, class E>
class [[nodiscard]] expected { ... };
|
Introduction
[[nodiscard]] was introduced in Andrew Tomazos's Proposal of [[unused]], [[nodiscard]] and [[fallthrough]] attributes (P0068R0) as a way to generate compiler warnings when [[nodiscard]]-marked functions (or functions with [[nodiscard]]-marked return types) are called without handling their return values. The feature was standardized in C++17 with Wording for [[nodiscard]] attribute (P0189R1).
As a rule, the standard encourages but does not mandate compiler warnings. [[nodiscard]] is no exception. This fact, combined with the as-if rule, implies standard library implementers are not bound at all by where the C++ standard uses [[nodiscard]] annotations. Despite this implementor freedom, annotations were added to the standard library in C++17 with Nicolai Josuttis's [[nodiscard]] in the Library (P0600R1). Josuttis proposed [[nodiscard]] placement as follows:
- For existing APIs:
- not using the return value always is a “huge mistake” (e.g., always resulting in resource leak)
- not using the return value is a source of trouble and easily can happen (not obvious that something is wrong)
- For new APIs (not been in the C++ standard yet):
- not using the return value is usually an error.
Following P0600R1's incorporation, the committee has not consistently followed Josuttis's recommendation for new APIs. Without an official policy, each new function's [[nodiscard]] annotation was subject to a separate debate, resulting in the present standard's [[nodiscard]] incoherence and time-consuming LEWG discussions.
To resolve inconsistencies and reduce committee debate time, we advocate for the following LEWG policy:
- Place [[nodiscard]] on functions where ignoring a return value is inevitably a severe defect, such as resource leakage.
- Place [[nodiscard]] on functions where overlooking the return value is a common mistake, such as function names frequently confused with others.
- Place [[nodiscard]] on types designed to communicate errors as function return values.
This paper discusses the principles driving this policy, surveys existing practices, considers alternatives, and proposes wording for SD-9.
Principles
No [[nodiscard]] policy can address all concerns, so we propose guiding principles to aid decision-making: minimize complexity, focus on the 90% use case, and center on outcomes.
Minimize complexity
Minimizing C++'s complexity makes it more approachable for new users and reduces the maintenance burden of written code, thus improving its longevity. This principle rules out placing [[nodiscard]] on almost all functions with non-void return types, even if ignoring these return values is likely a bug.
Focus on the 90% use case
Focusing on the 90% use case recognizes that concentrating on most people's problems rather than all people's problems will generally produce better outcomes, especially when simplicity is at stake. In this case, a handful of [[nodiscard]]'s placements is sufficient to generate warnings for the most common and severe bugs.
Consider these examples:
std::vector<int> v{...};
v.empty();
std::unique_ptr<X> x{...};
x.release();
std::async(job_x, &x, ...);
std::async(job_y, &y, ...);
calloc(size * sizeof(int));
Center on outcomes
Centering on outcomes is about considering the practical impact of a decision. What impact will a [[nodiscard]] policy have? We've identified two:
- Standard library implementations. Implementors are not obligated to follow [[nodiscard]] guidance. libstdc++ and Visual C++ make their own decisions, but libc++ mimics the standard's placement. The impact here is minimal.
- Training content. Training, books, and websites, including the incredibly popular cppreference.com, frequently reproduce function signatures in the standard library. These signatures, as a result, heavily influence coding style.
Whether or not the standard is intended to impact coding style, it does and the committee should pay attention to this impact. Do we want the standard to encourage, by example, a conservative placement of [[nodiscard]] annotations, placement of [[nodiscard]] almost everywhere, or to avoid using the feature entirely? It is our position that users are best served by the former.
Survey of existing practice
This section surveys existing [[nodiscard]] placement in the standard library, tooling, and the C++ Core guidelines. In short, the standard lacks consistency, tooling tends towards warning on all unused returns, and the C++ Core Guidelines have little to say on the subject. It is no wonder that users are frequently perplexed about what to do with the feature.
The standard library
The code snippets below show various signatures that either get or do not get the [[nodiscard]] attribute in the December 2023 C++ Working Draft (N4971)
.empty()
(~30 occurrences)
[[nodiscard]] bool map<T>::empty() const noexcept;
[[nodiscard]] bool path<T>::empty() const noexcept;
[[nodiscard]] constexpr bool basic_string_view<T>::empty() const noexcept;
This addresses .empty()'s confusion with .clear(). All .empty() instances were marked as [[nodiscard]].
- Allocation functions (~20 occurrences)
[[nodiscard]] void* operator new(size_t);
[[nodiscard]] void* operator new[](size_t);
[[nodiscard]] constexpr T* allocator<T>allocate(size_t n);
[[nodiscard]] void* pmr::allocate(size_t bytes, size_t alignment = max_align);
Discarding a value returned by an allocation function is undoubtedly a memory leak.
template<class F, class... Args>
[[nodiscard]] future<...> async(F&& f, Args&&... args);
Not capturing the return value for async
results in surprising synchronous execution.
- Standard error types (0 occurrences) -
template<class T, class E>
class expected { ... };
template<class E>
class unexpected { ... };
class error_code { ... };
class error_condition { ... };
enum class errc { ... };
None of the standard error types have a [[nodiscard]] attribute.
[[nodiscard]] void* operator new(size_t);
void* malloc(size_t size);
void* calloc(size_t nmemb, size_t size);
void* realloc(void* ptr, size_t size);
constexpr bool operator==(const vector<...>& x, const vector<...>& y);
constexpr bool operator==(const basic_string<...>& lhs, const basic_string<...>& rhs) noexcept;
[[nodiscard]] friend bool operator==(const stop_token& lhs, const stop_token& rhs) noexcept;
[[nodiscard]] friend bool operator==(const stop_source& lhs, const stop_source& rhs) noexcept;
id this_thread::get_id() noexcept;
[[nodiscard]] id jthread::get_id() const noexcept;
Clang Tidy provides the modernize-use-nodiscard flag, which suggests and automatically applies [[nodiscard]] to a codebase in specific scenarios, such as non-void non-template return const member function.
bool class_name::empty() const;
bool class_name::check(int i) const;
Moreover, the experimental flag bugprone-unused-return-value generates warnings when a user ignores the return value of specific Standard Library functions. An example is std::unique_ptr::release(), which may be confused with std::unique_ptr::reset().
Similarly to our proposed policy, this tool warns when standard error type return values are not handled:
std::expected<int, int> foo() { return {1}; }
void bar() { foo(); }
The C++ Core Guidelines
A recent GitHub issue in the C++ Core Guidelines repository, [[nodiscard]] advice is largely absent, points out that the C++ Core Guidelines does not mention [[nodiscard]] aside from not ignoring results of functions with this attribute. Considering section SL.2 ("SL.2: Prefer the standard library to other libraries"), the missing guideline from C++ Core Guidelines should be consistent with a LEWG policy.
The dicussion led to multiple possible guidelines that are still under debate:
A: Add [[nodiscard]] when it disambiguates what a function does or discarding the result is a safety issue.
B: Add [[nodiscard]] when it is likely to avoid the misuse of a function.
C: Add [[nodiscard]] when a function is impossible to use when the caller discards the result.
D: Add [[nodiscard]] when discarding the result of a function with side effects is not intended.
The guideline A uses the attribute only for critical situations (e.g., .empty()). In the opposite direction, the B version suggests to put [[nodiscard]] almost everywhere (e.g., even on operator==). The C wording limits the usage to specific cases where code may break (e.g., std::launder, std::async), while D aims to use [[nodiscard]] only on functions with side-effects, such read().
Alternatives considered
-
Place [[nodiscard]] on every function where discarding the result is likely a bug. Because this would result in [[nodiscard]] placement on almost every non-void returning function, it goes against the principle of minimizing complexity.
-
Do not use [[nodiscard]] in the standard library. This alternative goes against the principle of centering on outcomes. If people do not see [[nodiscard]] used in the standard library or relevant documentation (such as cppreference), then the positive impact the feature can have is diminished.
Wording for SD-9
Append to List of Standard Library Policies section of SD-9 (P2267R1: Library Evolution Policies):
X. [[nodiscard]] policy:
X.1. (P3162R0) Place [[nodiscard]] on functions where ignoring a return value is inevitably a severe defect, such as resource leakage.
X.2. (P3162R0) Place [[nodiscard]] on functions where overlooking the return value is a common mistake, such as function names frequently confused with others.
X.3. (P3162R0) Place [[nodiscard]] on types designed to communicate errors as function return values.
Conclusion
This paper demonstrated the need for a policy regulating [[nodiscard]] use in the standard library and proposed such a policy. It outlined the principles motivating this policy, analyzed existing practices, and considered alternatives.
References
- Christopher Di Bella. P2377R0: [[nodiscard]] in the Standard Library: Clause 23 Iterators library. https://wg21.link/P2377R0, 05/2021
- Hana Dusíková. P2351R0: Mark all library static cast wrappers as [[nodiscard]]. https://wg21.link/P2351R0, 04/2020
- Nicolai Josuttis. P0600R1: [[nodiscard]] in the Library, Rev1.
https://wg21.link/p0600r1, 11/2017.
- Inbal Levi et al. P2267R1: Library Evolution Policies.
- Andrew Tomazos. P0189R1: Wording for [[nodiscard]] attribute.
https://wg21.link/p0189r1, 2/2016.
- Andrew Tomazos. P0068R0: Proposal of [[unused]], [[nodiscard]] and [[fallthrough]] attributes. https://wg21.link/p0068r0, 9/2015.
- Daniel Towner, Ruslan Arutyunyan. P2993R1: Extend
<bit>
header function with overloads for std::simd
. https://wg21.link/P2933R1, 8/2023
https://wg21.link/P2267R1, 11/2023
- Ville Voutilainen. D2422R0: Remove nodiscard annotations from the standard library specification. https://isocpp.org/files/papers/D2422R0.html, 08/2021.
LEWG [[nodiscard]] policy
Document number: P3162R0
Date: 2024-02-22
Authors: Darius Neațu <dariusn@adobe.com>, David Sankel <dsankel@adobe.com>
Audience: Library Evolution
Abstract
The committee spends substantial time deciding whether or not [[nodiscard]] is appropriate for newly introduced standard functions, and the decisions made are often incoherent. We propose a policy that results in minimal syntactic overhead while retaining the detection of the most egregious defects.
Introduction
[[nodiscard]] was introduced in Andrew Tomazos's Proposal of [[unused]], [[nodiscard]] and [[fallthrough]] attributes (P0068R0) as a way to generate compiler warnings when [[nodiscard]]-marked functions (or functions with [[nodiscard]]-marked return types) are called without handling their return values. The feature was standardized in C++17 with Wording for [[nodiscard]] attribute (P0189R1).
As a rule, the standard encourages but does not mandate compiler warnings. [[nodiscard]] is no exception. This fact, combined with the as-if rule[1], implies standard library implementers are not bound at all by where the C++ standard uses [[nodiscard]] annotations. Despite this implementor freedom, annotations were added to the standard library in C++17 with Nicolai Josuttis's [[nodiscard]] in the Library (P0600R1). Josuttis proposed [[nodiscard]] placement as follows:
Following P0600R1's incorporation, the committee has not consistently followed Josuttis's recommendation for new APIs. Without an official policy, each new function's [[nodiscard]] annotation was subject to a separate debate, resulting in the present standard's [[nodiscard]] incoherence[2] and time-consuming LEWG discussions[3].
To resolve inconsistencies and reduce committee debate time, we advocate for the following LEWG policy:
This paper discusses the principles driving this policy, surveys existing practices, considers alternatives, and proposes wording for SD-9.
Principles
No [[nodiscard]] policy can address all concerns, so we propose guiding principles to aid decision-making: minimize complexity, focus on the 90% use case, and center on outcomes.
Minimize complexity
Minimizing C++'s complexity makes it more approachable for new users and reduces the maintenance burden of written code, thus improving its longevity. This principle rules out placing [[nodiscard]] on almost all functions with non-void return types, even if ignoring these return values is likely a bug.
Focus on the 90% use case
Focusing on the 90% use case recognizes that concentrating on most people's problems rather than all people's problems will generally produce better outcomes, especially when simplicity is at stake. In this case, a handful of [[nodiscard]]'s placements is sufficient to generate warnings for the most common and severe bugs.
Consider these examples:
Center on outcomes
Centering on outcomes is about considering the practical impact of a decision. What impact will a [[nodiscard]] policy have? We've identified two:
Whether or not the standard is intended to impact coding style, it does and the committee should pay attention to this impact. Do we want the standard to encourage, by example, a conservative placement of [[nodiscard]] annotations, placement of [[nodiscard]] almost everywhere, or to avoid using the feature entirely? It is our position that users are best served by the former.
Survey of existing practice
This section surveys existing [[nodiscard]] placement in the standard library, tooling, and the C++ Core guidelines. In short, the standard lacks consistency, tooling tends towards warning on all unused returns, and the C++ Core Guidelines have little to say on the subject. It is no wonder that users are frequently perplexed about what to do with the feature[6].
The standard library
The code snippets below show various signatures that either get or do not get the [[nodiscard]] attribute in the December 2023 C++ Working Draft (N4971)
.empty()
(~30 occurrences)This addresses .empty()'s confusion with .clear(). All .empty() instances were marked as [[nodiscard]].
Discarding a value returned by an allocation function is undoubtedly a memory leak.
async
(2 occurrences)Not capturing the return value for
async
results in surprising synchronous execution.None of the standard error types have a [[nodiscard]] attribute.
Tooling: Clang Tidy
Clang Tidy provides the modernize-use-nodiscard flag, which suggests and automatically applies [[nodiscard]] to a codebase in specific scenarios, such as non-void non-template return const member function.
Moreover, the experimental flag bugprone-unused-return-value generates warnings when a user ignores the return value of specific Standard Library functions. An example is std::unique_ptr::release(), which may be confused with std::unique_ptr::reset().
Similarly to our proposed policy, this tool warns when standard error type return values are not handled:
The C++ Core Guidelines
A recent GitHub issue in the C++ Core Guidelines repository, [[nodiscard]] advice is largely absent, points out that the C++ Core Guidelines does not mention [[nodiscard]] aside from not ignoring results of functions with this attribute. Considering section SL.2 ("SL.2: Prefer the standard library to other libraries"), the missing guideline from C++ Core Guidelines should be consistent with a LEWG policy.
The dicussion led to multiple possible guidelines that are still under debate:
The guideline A uses the attribute only for critical situations (e.g., .empty()). In the opposite direction, the B version suggests to put [[nodiscard]] almost everywhere (e.g., even on operator==). The C wording limits the usage to specific cases where code may break (e.g., std::launder, std::async), while D aims to use [[nodiscard]] only on functions with side-effects, such read().
Alternatives considered
Place [[nodiscard]] on every function where discarding the result is likely a bug. Because this would result in [[nodiscard]] placement on almost every non-void returning function, it goes against the principle of minimizing complexity.
Do not use [[nodiscard]] in the standard library. This alternative goes against the principle of centering on outcomes. If people do not see [[nodiscard]] used in the standard library or relevant documentation (such as cppreference), then the positive impact the feature can have is diminished.
Wording for SD-9
Append to List of Standard Library Policies section of SD-9 (P2267R1: Library Evolution Policies):
Conclusion
This paper demonstrated the need for a policy regulating [[nodiscard]] use in the standard library and proposed such a policy. It outlined the principles motivating this policy, analyzed existing practices, and considered alternatives.
References
https://wg21.link/p0600r1, 11/2017.
https://wg21.link/p0189r1, 2/2016.
<bit>
header function with overloads forstd::simd
. https://wg21.link/P2933R1, 8/2023https://wg21.link/P2267R1, 11/2023
For details on the "as-if" rule, see paragraph 1 of [intro.abstract] in the December 2023 C++ Working Draft (N4971) and its footnote ↩︎
For a recent example, see the 2023 Kona discussion on [[nodiscard]] placement for Extend <bit> header function with overloads for std::simd (P2993R1) ↩︎
LEWG related papers: Christopher Di Bella's P2377R0: [[nodiscard]] in the Standard Library: Clause 23 Iterators library, Ville Voutilainen's D2422R0: Remove nodiscard annotations from the standard library specification, Hana Dusíková's P2351R0: Mark all library static cast wrappers as nodiscard. ↩︎
Note libstdc++'s placement of [[nodiscard]] on both vector::capacity and vector::empty where the standard has placement only on the latter. The same is true for Visual C++ ↩︎
See vector::empty() and vector::capacity() inside libc++. ↩︎
See this stackoverflow discussion for an example ↩︎