C++ / FUNCTIONS
nodiscard and stating what callers must not ignore
Mark functions, classes, and enums [[nodiscard]] so the compiler flags call sites that throw away a result the caller was required to use.
What you will learn
- Mark [[nodiscard]] on functions whose return value is the only reason to call them
- Put the attribute on the header declaration, not only on the .cpp definition
- Mark a class or enum [[nodiscard]] to cover every function returning it by value
- Silence an intended discard with (void) or std::ignore, not a dummy variable
Understanding nodiscard and stating what callers must not ignore
C++ lets you call a function and drop its result without a word of complaint. For a function whose only effect is its return value, that call is dead code: `has_funds(balance, 50);` as a statement computes an answer and deletes it. For a function that hands back an error code or a resource handle, dropping the value destroys the only copy of that information, so the failure is never noticed or the resource is never released. `[[nodiscard]]`, added in C++17, is an attribute you put on the declaration to ask the compiler to diagnose exactly this situation at the call site.
The mental model is a question about the direction information flows: if the caller throws this value away, is what remains still useful? A pure query like `empty()` or a transform that returns a new string leaves nothing behind, so discarding it is always a bug and the function belongs behind `[[nodiscard]]`. A function whose side effect is the point, and whose return value is a convenience, does not; marking those produces warnings people learn to ignore, which is worse than no warning. Note that the attribute is a property of the declaration, not of the return type, so it says nothing about ownership or lifetime, only that the caller is expected to look at the value.
Mechanically, the warning fires only when the call appears as a discarded-value expression: a bare statement, or the left side of a comma. Initializing a variable from the call satisfies the attribute even if you never read the variable, and an explicit cast to `void` is the standard escape hatch for a discard you actually meant. The standard only recommends the diagnostic rather than requiring it, so it arrives as `-Wunused-result` on GCC and Clang and as C4834 on MSVC, and you need `-Werror=unused-result` if you want it to stop a build. Because the caller must see the attribute, it goes on the declaration in the header, before the return type, not on the out-of-line definition.
<iostream>
<string>
// Pure check: the answer is the entire point of calling it.
[[nodiscard]] bool has_funds(int balance, int amount) {
return balance >= amount;
}
// The new balance is returned, not stored, so dropping it loses the charge.
[[nodiscard]] int charge(int balance, int amount) {
return balance - amount;
}
// No attribute: printing is the point, the byte count is a convenience.
int log_line(const std::string& text) {
std::cout << text << '\n';
return static_cast<int>(text.size());
}
int main() {
int balance = 120;
if (has_funds(balance, 50)) {
balance = charge(balance, 50);
log_line("charged 50");
}
if (has_funds(balance, 100)) {
balance = charge(balance, 100);
log_line("charged 100");
} else {
log_line("declined 100");
}
// charge(balance, 50); // warning: return value ignored
// has_funds(balance, 50); // warning: the check would do nothing
(void)has_funds(balance, 0); // deliberate discard, accepted by the cast
log_line("final balance " + std::to_string(balance));
}
[[nodiscard]] turns "the caller must use this value" from a comment into a property of the declaration that the compiler checks at every call site that throws the value away.
Worked examples
Marking the type instead of every function
A [[nodiscard]] class type makes every function that returns it by value protected, without touching those functions.
<iostream>
// The attribute sits between the class-key and the name.
struct [[nodiscard]] Parsed {
bool ok;
int value;
};
Parsed parse(const char* text) { // no attribute needed here
int n = 0;
for (const char* p = text; *p; ++p) {
if (*p < '0' || *p > '9') return Parsed{false, 0};
n = n * 10 + (*p - '0');
}
return Parsed{*text != '\0', n};
}
int main() {
Parsed a = parse("407");
std::cout << a.ok << ' ' << a.value << '\n';
// parse("4x7"); // warning: ignoring a value of nodiscard type Parsed
(void)parse("4x7"); // silenced on purpose
Parsed b = parse("4x7");
std::cout << b.ok << ' ' << b.value << '\n';
}
Example explained
Line 1`struct [[nodiscard]] Parsed` marks the type, so any function returning `Parsed` by value is covered retroactively.
Line 2`parse` itself carries no attribute, which is how you retrofit a whole API of factory functions in one edit.
Line 3The diagnostic names the discarded call, not the declaration, because the call site is where the information is lost.
Line 4Type-level marking applies to `Parsed` returned by value only; a function returning `const Parsed&` is not a nodiscard call.
A reason string, and std::ignore
C++20 lets the attribute carry text explaining the mistake, and shows that assigning to std::ignore is not a discard.
<iostream>
<string>
<tuple>
// The string form requires C++20; it is printed with the warning.
[[nodiscard("upper returns a new string, the argument is left alone")]]
std::string upper(std::string s) {
for (char& c : s) {
if (c >= 'a' && c <= 'z') c = static_cast<char>(c - 'a' + 'A');
}
return s;
}
int main() {
std::string name = "ada";
// upper(name); // warning quotes the reason text
std::cout << upper(name) << '\n'; // value used as a stream operand
std::cout << name << '\n'; // the original is untouched
std::ignore = upper(name); // assigned, therefore not discarded
}
Example explained
Line 1The reason string exists because the fix for `upper(name);` is not obvious: the caller wanted in-place modification.
Line 2Passing the result to `operator<<` uses the value, so the call is not a discarded-value expression.
Line 3`std::ignore = upper(name);` binds the string to an assignment operator, which satisfies the attribute while reading as intentional.
Line 4The second line proves the point of the warning: `name` still holds "ada", so a bare `upper(name);` would have done nothing.
Error enums, and why storing is enough
A [[nodiscard]] enum forces the return code to be looked at, but only at the syntactic level.
<iostream>
// The attribute goes after the enum-key, before the name.
enum class [[nodiscard]] Err { none, range };
Err set_port(int p, int& out) {
if (p < 1 || p > 65535) return Err::range;
out = p;
return Err::none;
}
int main() {
int port = 0;
if (set_port(70000, port) != Err::none) {
std::cout << "rejected 70000, port still " << port << '\n';
}
Err e = set_port(8080, port); // stored: no nodiscard warning
std::cout << "port " << port << '\n';
std::cout << "code " << static_cast<int>(e) << '\n';
}
Example explained
Line 1`set_port` writes through `out` only on success, so ignoring the returned `Err` would leave `port` silently unchanged.
Line 2Comparing the call against `Err::none` uses the value, so the first call is accepted.
Line 3`Err e = set_port(...)` also uses the value; had `e` never been read, `-Wunused-variable` would be the diagnostic, not the nodiscard one.
Line 4`static_cast<int>(e)` is needed because a scoped enum has no implicit conversion for `operator<<`.
Important notes
The standard only recommends the diagnostic, so `[[nodiscard]]` never makes a program ill-formed by itself; `-Werror=unused-result` is what actually keeps the bug out of a build.
The attribute belongs to the declared function, not to the function type, so calling the same function through a function pointer or a `std::function` loses the check entirely.
Common mistakes
Putting `[[nodiscard]]` on the out-of-line definition in the .cpp file while callers include only the header: nobody ever sees a warning, and the two declarations now disagree between translation units, which the standard makes ill-formed with no diagnostic required.
Making the warning go away instead of fixing the call, by writing `(void)has_funds(balance, amount);` or assigning to a scratch variable: the code compiles and the unchecked charge goes through exactly as before, only now with a comment-free cast hiding it.
Marking every function that returns anything, including mutating ones whose return value is a convenience: the noise trains the team to add `-Wno-unused-result`, which removes the warning from the handful of functions where it mattered.
Try it yourself
Change, predict, then run
Write `[[nodiscard]] bool try_divide(int a, int b, int& out)` that returns false when `b` is zero, then call it once inside an `if` and once as a bare statement, and read the exact wording your compiler prints. Change the bare call to `(void)try_divide(...)` and confirm the warning disappears even though the bug does not.
Open the C++ workspaceCheck your understanding
A header declares `[[nodiscard]] int reserve(int n);`. A caller writes `int got = reserve(8);` and never reads `got` anywhere. What does the compiler report?
- No nodiscard warning, since the initialization used the value; an unused-variable warning may fire instead
- A nodiscard warning, since the returned value is never actually read by the program
- A hard error, because a nodiscard result must be inspected before the variable goes out of scope
- Nothing, because [[nodiscard]] only has an effect when it is applied to a class or enum type
Show answer
The attribute is defined in terms of discarded-value expressions, and initializing a variable is a use of the value, so the call site is clean no matter what happens to `got` afterwards. Option 1 is tempting because forcing the value to be checked was the intent, but the compiler only inspects the shape of the call expression, not whether the variable is ever read; catching the dead variable is the separate job of -Wunused-variable.