C++ [[nodiscard]] Guide: Ignore Return Values Safely | Errors, RAII & Reasons [#41-2]
이 글의 핵심
C++ [[nodiscard]] tells the compiler to warn when a return value is discarded—covering bool status codes, smart pointers, optional/expected, async futures, and C++20 reason messages on types and constructors.
Introduction: “We didn’t check the return value—and got a bug”
Real-world problem scenarios
Scenario 1: Missed initialization failure
bool init() returns false when DB connection or config load fails. If the caller only writes init(); and ignores the result, the program continues after failed initialization and may later crash with null pointer access. Root cause: the return value was ignored.
Scenario 2: Ignoring a unique_ptr return → leak or logic bug
std::unique_ptr<Resource> create() allocates and returns a resource. If the caller only runs create(); and does not store the result, the unique_ptr is destroyed immediately and the resource is released—but if the intent was to use the connection, “create and throw away” is usually a bug. The same applies to shared_ptr or custom RAII returns: ignoring the return is almost always a mistake.
Scenario 3: Error code not checked
int connect(const char* host) returns 0 on success and a negative error code on failure. If the caller only runs connect("db.example.com"); without checking, execution may continue after a failed connection and corrupt data or crash.
Scenario 4: Ignoring optional/expected
std::optional<User> findUser(int id) returns std::nullopt when the user is missing. If the caller only runs findUser(42); without binding the result, there is no chance to test “is there a value?”—leading to null dereferences or wrong assumptions.
Scenario 5: Ignoring the std::async return value
std::async(std::launch::async, task) returns a std::future. If that future is not stored, the asynchronous work may not be joined as intended, the destructor can block unexpectedly, and exceptions may not propagate— they can be “swallowed.”
Why use nodiscard?
Discarded return values are hard for static analysis to flag reliably and easy to miss in code review. [[nodiscard]] lets the compiler report misuse at compile time, reducing runtime crashes, leaks, and security issues. Alternatives like naming functions …Check or documenting “must check return” have no enforcement.
This article covers:
- Meaning of [[nodiscard]]: “do not discard the return value”
- What it applies to: functions,
enum class, class types, constructors (C++20), lambdas - Full examples: ignored returns, error codes, RAII types,
nodiscard("reason"), constructors - Common mistakes and fixes
- Production patterns: CI and Clang-Tidy
A quick analogy
As in other articles on this site: templates are like molds, smart pointers are like self-cleaning helpers, and RAII is like an automatic door—resources are tied to scope and cleaned up on exit.
Practical note: this article is grounded in real issues seen on large C++ codebases, including pitfalls and debugging tips not always spelled out in books.
Table of contents
- Problem scenarios in detail
- What is [[nodiscard]]?
- Examples: ignored return values
- Examples: ignored error codes
- RAII types and nodiscard
nodiscard("reason")(C++20)- Common errors and fixes
- Best practices
- Production patterns
- Checklist
- Summary
1. Problem scenarios in detail
Scenario A: Ignoring init()’s return
// ❌ Wrong: initialization failure is ignored
bool init() {
if (!loadConfig()) return false;
if (!connectDB()) return false;
return true;
}
void main_loop() {
init(); // Failure is ignored!
run(); // Runs even if DB never connected → crash
}
Note: Even if you rename the function to suggest side effects only, APIs that return success/failure as a value especially benefit from [[nodiscard]].
Fix: Add [[nodiscard]] so discarding the return triggers a compiler warning.
// ✅ Correct
[[nodiscard]] bool init() {
if (!loadConfig()) return false;
if (!connectDB()) return false;
return true;
}
void main_loop() {
if (!init()) {
log_error("Initialization failed");
return;
}
run();
}
Scenario B: Ignoring an error code
// ❌ Wrong: error code not checked
int openFile(const char* path) {
FILE* f = fopen(path, "r");
if (!f) return -errno;
// ...
return fd;
}
void process() {
openFile("data.txt"); // Failure ignored
// Uses fd when it may be invalid → crash
}
Fix: Use [[nodiscard]] to force checking the error code.
// ✅ Correct
[[nodiscard]] int openFile(const char* path) {
FILE* f = fopen(path, "r");
if (!f) return -errno;
// ...
return fd;
}
void process() {
int fd = openFile("data.txt");
if (fd < 0) {
log_error("Failed to open file: %d", fd);
return;
}
// use fd
}
Scenario C: Ignoring an RAII resource return
// ❌ Wrong: unique_ptr ignored
std::unique_ptr<Connection> createConnection() {
return std::make_unique<Connection>();
}
void setup() {
createConnection(); // Created then discarded—intent unclear
// To use the connection it must be stored somewhere…
}
Fix: Use [[nodiscard]] so the resource return cannot be silently dropped.
// ✅ Correct
[[nodiscard]] std::unique_ptr<Connection> createConnection() {
return std::make_unique<Connection>();
}
void setup() {
auto conn = createConnection();
if (conn) {
conn->connect();
}
}
Scenario D: Ignoring an optional return
// ❌ Wrong: optional ignored
std::optional<User> findUser(int id) {
auto it = db.find(id);
if (it == db.end()) return std::nullopt;
return it->second;
}
void display(int id) {
findUser(id); // Result not bound!
// To show user info you need the result…
}
Fix: Use [[nodiscard]] to encourage handling the optional.
// ✅ Correct
[[nodiscard]] std::optional<User> findUser(int id) {
auto it = db.find(id);
if (it == db.end()) return std::nullopt;
return it->second;
}
void display(int id) {
auto user = findUser(id);
if (user) {
showUser(*user);
} else {
showError("User not found");
}
}
Scenario E: Ignoring a std::async future
// ❌ Wrong: future ignored
void run_async() {
std::async(std::launch::async, [] {
throw std::runtime_error("async error");
});
// No future stored → exceptions may be lost
// Future destructor may block
}
Fix: std::async is already [[nodiscard]] in the standard library; apply the same idea to your own async helpers.
// ✅ Correct
[[nodiscard]] std::future<int> computeAsync(int x) {
return std::async(std::launch::async, [x]() {
return heavyComputation(x);
});
}
void run() {
auto fut = computeAsync(42);
int result = fut.get(); // use result
}
Scenario F: Ignoring hash/crypto output
// ❌ Wrong: hash not used for verification
std::array<uint8_t, 32> computeSHA256(const void* data, size_t len);
void verifyIntegrity(const std::vector<uint8_t>& payload) {
computeSHA256(payload.data(), payload.size()); // result ignored!
// Must compare to stored hash—no value to compare → verification skipped
}
Fix: Mark security- and integrity-sensitive functions [[nodiscard]] so results must be used.
// ✅ Correct
[[nodiscard]] std::array<uint8_t, 32> computeSHA256(const void* data, size_t len);
void verifyIntegrity(const std::vector<uint8_t>& payload,
const std::array<uint8_t, 32>& expected) {
auto hash = computeSHA256(payload.data(), payload.size());
if (hash != expected) {
throw std::runtime_error("Integrity check failed");
}
}
Scenario G: “Discarding” a constructor (RAII bug)
// ❌ Wrong: lock acquired then immediately released
class ScopedLock {
std::mutex& mtx_;
public:
explicit ScopedLock(std::mutex& m) : mtx_(m) { mtx_.lock(); }
~ScopedLock() { mtx_.unlock(); }
};
void criticalSection() {
std::mutex mtx;
ScopedLock(mtx); // Temporary: ctor/dtor → unlock
// Lock not held below → data race!
doCriticalWork();
}
Fix: In C++20, class [[nodiscard]] can warn when a guard object is created as a discarded temporary.
// ✅ Correct (C++20)
class [[nodiscard]] ScopedLock {
std::mutex& mtx_;
public:
explicit ScopedLock(std::mutex& m) : mtx_(m) { mtx_.lock(); }
~ScopedLock() { mtx_.unlock(); }
};
void criticalSection() {
std::mutex mtx;
ScopedLock lock(mtx); // Named object → lock held for scope
doCriticalWork();
}
2. What is [[nodiscard]]?
“Do not discard the return value”
[[nodiscard]] is an attribute that makes the compiler warn (or error) when the return value of a function or type is discarded. It was added in C++17; C++20 added reason strings and applying nodiscard to constructors.
How compilers check nodiscard (conceptual model):
Source:
[[nodiscard]] bool init() {
return connectDB();
}
int main() {
init(); // return value discarded
}
Compiler AST:
FunctionDecl: init
- return type: bool
- attributes: [[nodiscard]]
- body: { return connectDB(); }
CallExpr: init() in main
- callee: init
- return type: bool
- parent statement: ExprStmt
- value used: NO ← important
Semantic analysis:
1. Visit CallExpr:
if (call->getType() != void) {
check whether return value is used
}
2. Is the return value used?
Code:
init(); // ExprStmt
→ Not used in another expression
→ value_used = false
3. nodiscard on the callee:
FunctionDecl* func = call->getDirectCallee();
if (func->hasAttr<WarnUnusedResultAttr>()) {
if (!value_used) {
emit_warning("ignoring return value of function declared with 'nodiscard'");
}
}
Warning:
warning: ignoring return value of function declared with 'nodiscard' [-Wunused-result]
init();
^~~~~~
Compiler notes:
GCC:
- -Wunused-result
- Mapped from warn_unused_result
- Checked during semantic analysis
Clang:
- -Wunused-result
- warn_unused_result
- Sema::DiagnoseUnusedExprResult()
MSVC:
- Warning C4834
- Related to _Check_return_
When the return value counts as “used”:
auto x = init(); // assigned
if (init()) { } // condition
return init(); // returned
foo(init()); // argument
x = init() ? 1 : 2; // ternary
x && init(); // logical ops (short-circuit)
When it is discarded:
init(); // ExprStmt
{ init(); } // in block
for (...) { init(); } // in loop
void cast (suppress warning):
(void)init();
AST:
CStyleCastExpr: (void)
- child: CallExpr: init()
- target type: void
Compiler treats this as intentional discard → warning suppressed
Type-level nodiscard:
enum class [[nodiscard]] Status { Ok, Error };
Status connect() {
return Status::Ok;
}
int main() {
connect(); // warning: Status is nodiscard
}
Steps:
1. Return type of connect(): Status
2. Status definition has [[nodiscard]]
3. Return value discarded → warning
Constructor nodiscard (C++20):
class [[nodiscard]] Guard {
Guard() { }
~Guard() { }
};
int main() {
Guard(); // C++20: warning
}
AST:
CXXTemporaryObjectExpr: Guard()
- type: Guard (nodiscard)
- materialized: NO
- parent: ExprStmt
Checks:
1. Temporary object
2. Guard is nodiscard
3. Not bound to a name
4. Immediate destruction → possible RAII misuse
5. Warning
-Werror=unused-result:
Promote warning to error:
g++ -Werror=unused-result main.cpp
→ error: ignoring return value of function declared with 'nodiscard'
CI/CD:
→ build fails on nodiscard violations
→ can block merges
flowchart TB
subgraph Call[Call site]
A["nodiscard function call"] --> B{Return value used?}
B -->|Yes| C[OK]
B -->|No| D[Compiler warning or error]
D --> E[Catch bugs early]
end
With vs without nodiscard:
| Situation | Without nodiscard | With nodiscard |
|---|---|---|
init(); (discard) | May compile; runtime failure | Warning or error |
createResource(); (drop handle) | Leak or logic bug | Early warning |
findUser(42); (ignore optional) | Null deref risk | Early warning |
sequenceDiagram
participant Dev as Developer
participant Comp as Compiler
participant Code as Code
Dev->>Code: init(); (discard return)
Code->>Comp: [[nodiscard]] bool init()
Comp->>Comp: Check use of return value
Comp->>Dev: Warning: discarded nodiscard result
Dev->>Code: if (!init()) return;
Comp->>Dev: Build succeeds
What you can attach it to
| Target | C++ version | Notes |
|---|---|---|
| Function | C++17 | On the function declaration |
| enum class | C++17 | Applies to all functions returning that type |
| struct/class | C++17 | Applies when returning that type by value |
| Constructor | C++20 | Warns when the created object is discarded |
| Lambda | C++17 | e.g. [[]] [[nodiscard]] { return x; } |
| Reason string | C++20 | [[nodiscard("memory leak risk")]] |
Suppressing with a void cast
To intentionally discard while silencing the warning, use (void)expr:
[[nodiscard]] bool init();
void test() {
(void)init(); // intentional discard
}
3. Examples: ignored return values
Example 1: bool status function
[[nodiscard]] bool loadConfig(const std::string& path) {
std::ifstream f(path);
if (!f) return false;
// parse...
return true;
}
void startup() {
if (!loadConfig("config.json")) {
std::cerr << "Failed to load config\n";
std::exit(1);
}
}
Example 2: Computed result
[[nodiscard]] double computeHash(const std::vector<uint8_t>& data) {
double hash = 0;
for (auto b : data) hash = hash * 31 + b;
return hash;
}
void verify() {
auto data = readFile("data.bin");
double h = computeHash(data); // return value used
if (h != expectedHash) { /* ... */ }
}
Example 3: Factory function
[[nodiscard]] std::unique_ptr<Database> createDatabase(const Config& cfg) {
return std::make_unique<SqliteDatabase>(cfg.path);
}
int main() {
auto db = createDatabase(config);
if (!db) return 1;
db->execute("SELECT 1");
}
Example 4: Getters (pure queries)
class UserRepository {
public:
[[nodiscard]] std::optional<User> findById(int id) const;
[[nodiscard]] std::vector<User> search(const std::string& query) const;
[[nodiscard]] size_t count() const noexcept;
};
4. Examples: ignored error codes
Example 1: nodiscard on enum class (C++17)
enum class [[nodiscard]] ErrorCode {
Ok,
NotFound,
PermissionDenied,
IoError
};
ErrorCode openFile(const std::string& path, FILE*& out) {
out = fopen(path.c_str(), "r");
if (!out) return ErrorCode::IoError;
return ErrorCode::Ok;
}
void process() {
FILE* f = nullptr;
if (openFile("data.txt", f) != ErrorCode::Ok) { // must check
return;
}
// use f
}
Example 2: Result struct (C++17)
struct [[nodiscard]] Result {
int value;
bool success;
};
Result parseInteger(const std::string& s) {
try {
return {std::stoi(s), true};
} catch (...) {
return {0, false};
}
}
void use() {
auto r = parseInteger("42");
if (!r.success) return;
int x = r.value;
}
Example 3: std::expected style (C++23)
// C++23 std::expected or a small wrapper
template<typename T, typename E>
struct [[nodiscard]] Expected {
T value;
E error;
bool has_value;
// ...
};
[[nodiscard]] Expected<int, std::string> parse(const std::string& s) {
// ...
}
Example 4: errno-style integer
// 0 = success, negative = errno
[[nodiscard]] int connect(const char* host, int port) {
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) return -errno;
// connect...
return fd >= 0 ? 0 : -errno;
}
5. RAII types and nodiscard
Example 1: unique_ptr factory
[[nodiscard]] std::unique_ptr<Buffer> createBuffer(size_t size) {
return std::make_unique<Buffer>(size);
}
void use() {
auto buf = createBuffer(1024);
buf->write(data);
}
Example 2: shared_ptr accessor
[[nodiscard]] std::shared_ptr<Cache> getGlobalCache() {
static auto cache = std::make_shared<Cache>();
return cache;
}
Example 3: Custom RAII wrapper
class [[nodiscard]] ScopedLock {
std::mutex& mtx_;
public:
explicit ScopedLock(std::mutex& m) : mtx_(m) { mtx_.lock(); }
~ScopedLock() { mtx_.unlock(); }
ScopedLock(const ScopedLock&) = delete;
};
void criticalSection() {
std::mutex mtx;
ScopedLock lock(mtx); // if discarded → immediate unlock → bug
// ...
}
Example 4: File handle wrapper
class [[nodiscard]] FileHandle {
FILE* f_;
public:
explicit FileHandle(const char* path) : f_(fopen(path, "r")) {}
~FileHandle() { if (f_) fclose(f_); }
bool isOpen() const { return f_ != nullptr; }
FILE* get() const { return f_; }
};
void readFile() {
FileHandle f("data.txt"); // if discarded, odd lifetime
if (!f.isOpen()) return;
// ...
}
Example 5: optional return
[[nodiscard]] std::optional<std::string> getEnv(const char* name) {
const char* v = std::getenv(name);
if (!v) return std::nullopt;
return std::string(v);
}
void use() {
auto path = getEnv("HOME");
if (path) {
std::cout << "Home: " << *path << "\n";
}
}
6. nodiscard("reason") (C++20)
Clarify warnings with a message
Since C++20, [[nodiscard("string")]] can embed a reason in the diagnostic.
[[nodiscard("Ignoring the returned unique_ptr leaks or drops ownership of the resource")]]
std::unique_ptr<Resource> createResource() {
return std::make_unique<Resource>();
}
[[nodiscard("You must check the result or initialization failure may go unnoticed")]]
bool init() {
return connect();
}
void bad() {
createResource(); // warning includes the string above
init();
}
Nodiscard on constructors (C++20)
Warns when the constructed object is discarded—useful to catch “temporary guard” mistakes.
class [[nodiscard]] Guard {
public:
Guard() { acquire(); }
~Guard() { release(); }
};
void bad() {
Guard(); // warning: Guard temporary discarded
}
void good() {
Guard g; // OK
}
“Strategic value” example (cppreference style)
[[nodiscard("The computed value must be used")]] int strategic_value(int x, int y) {
return x ^ y;
}
int main() {
strategic_value(4, 2); // warning
auto z = strategic_value(0, 0); // OK
return z;
}
Constructors with nodiscard (C++20)—full example
In C++20 you can mark constructors; marking the class [[nodiscard]] warns when a temporary of that type is created and discarded—preventing “lock guard created and immediately destroyed.”
// C++20: RAII guard — warn if not bound to a variable
class [[nodiscard]] MutexGuard {
std::mutex& mtx_;
public:
explicit MutexGuard(std::mutex& m) : mtx_(m) { mtx_.lock(); }
~MutexGuard() { mtx_.unlock(); }
MutexGuard(const MutexGuard&) = delete;
MutexGuard& operator=(const MutexGuard&) = delete;
};
void bad_example() {
std::mutex mtx;
MutexGuard(mtx); // warning: temporary MutexGuard discarded
// Destroyed immediately → lock released → data race below
doWork();
}
void good_example() {
std::mutex mtx;
MutexGuard guard(mtx); // OK: named object
doWork(); // unlocked when guard is destroyed
}
Types that benefit from constructor nodiscard:
- Lock guards (
std::lock_guard,std::unique_lock, etc.) - File/socket handle wrappers
- Transaction guards (BEGIN on construct, COMMIT/ROLLBACK on destroy)
- Any RAII type where “temporary only” is almost always wrong
7. Common errors and fixes
Issue 1: “I don’t get a warning”
Symptom: [[nodiscard]] is present but no diagnostic appears.
Cause: The warning group may be off by default or treated as low priority.
Fix:
# GCC/Clang
-Wunused-result
# Treat as error
-Werror=unused-result
# CMake example
target_compile_options(myapp PRIVATE -Wunused-result -Werror=unused-result)
Issue 2: “nodiscard doesn’t apply to reference returns”
Symptom: ErrorCode& foo() does not warn when the result is discarded, even if the enum type is nodiscard.
Cause: Type-level nodiscard applies when the enum (or type) is returned by value. Reference returns are not “new values” in the same way.
Fix:
enum class [[nodiscard]] ErrorCode { Ok, Fail };
ErrorCode& getLastError(); // reference → typically no discard warning
void f() {
getLastError(); // OK (reference)
}
ErrorCode getError() { return ErrorCode::Ok; }
void g() {
getError(); // warning
}
Issue 3: “I put nodiscard on void”
Symptom: No meaningful effect.
Fix: Do not use [[nodiscard]] on void; there is nothing to discard.
// ❌ Pointless
[[nodiscard]] void log(const std::string& msg);
// ✅ Use on non-void returns
[[nodiscard]] bool tryLog(const std::string& msg);
Issue 4: “I mean to ignore it but get a warning”
Symptom: Tests or rare paths intentionally discard the result.
Fix:
[[nodiscard]] bool init();
void test_init_failure() {
(void)init(); // intentional
}
// Some code uses std::ignore as a pattern
#include <tuple>
void test2() {
std::ignore = init(); // may suppress on some compilers
}
Issue 5: “Override is missing nodiscard”
Symptom: Base virtual has [[nodiscard]] but the override does not.
Fix: Repeat [[nodiscard]] on the override for consistency.
struct Base {
[[nodiscard]] virtual bool validate() const { return true; }
};
struct Derived : Base {
[[nodiscard]] bool validate() const override { return check(); }
};
Issue 6: “Templates and nodiscard”
Symptom: Unsure whether templates need it.
Fix: If discarding the return is wrong for the template, mark it [[nodiscard]].
template<typename T>
[[nodiscard]] std::optional<T> find(const std::vector<T>& vec, const T& key) {
auto it = std::find(vec.begin(), vec.end(), key);
if (it == vec.end()) return std::nullopt;
return *it;
}
Issue 7: “Lambdas and nodiscard”
Symptom: Callers can discard the lambda’s return value.
Fix:
auto getValue = [] [[nodiscard]] () { return 42; };
void bad() {
getValue(); // warning
}
void good() {
int x = getValue(); // OK
}
Issue 8: “Confusion with std::vector::empty()”
Symptom: vec.empty(); warns because empty() is [[nodiscard]] in the standard library.
Fix: Use the result, e.g. if (vec.empty()) { ... }.
std::vector<int> vec;
if (vec.empty()) { // OK
// ...
}
vec.empty(); // warning: discarded nodiscard result
Issue 9: “Macro breaks C++14 builds”
Symptom: #define NODISCARD [[nodiscard]] fails on older standards.
Fix: Branch on __cplusplus.
#if __cplusplus >= 201703L
#define NODISCARD [[nodiscard]]
#else
#define NODISCARD
#endif
NODISCARD bool init(); // C++17+: attribute; C++14: empty
Issue 10: “Only one constructor should be nodiscard”
Symptom: You want nodiscard on a specific constructor, not the whole class.
Fix: In C++20, put [[nodiscard]] on that constructor only.
class ResourceGuard {
public:
[[nodiscard]] ResourceGuard(); // this ctor only
ResourceGuard(const ResourceGuard&); // copy ctor not nodiscard
};
Issue 11: “Where does [[nodiscard]] go on long return types?”
Symptom: Confusion with std::optional<std::unique_ptr<T>> etc.
Fix: Place [[nodiscard]] before the function name / at the start of the declaration—not after the return type.
// ✅ Correct
[[nodiscard]] std::optional<std::unique_ptr<Resource>> tryCreate();
// ❌ Invalid placement
std::optional<std::unique_ptr<Resource>> [[nodiscard]] tryCreate();
8. Best practices
1. Functions returning status or error codes
[[nodiscard]] bool init();
[[nodiscard]] int connect(const char* host);
[[nodiscard]] ErrorCode openFile(const std::string& path);
2. Functions returning resources
[[nodiscard]] std::unique_ptr<Resource> create();
[[nodiscard]] std::shared_ptr<Cache> getCache();
[[nodiscard]] std::expected<Data, Error> fetch();
3. optional / expected returns
[[nodiscard]] std::optional<User> findUser(int id);
[[nodiscard]] std::expected<Config, std::string> loadConfig();
4. Pure computations
[[nodiscard]] double compute(const Data& d);
[[nodiscard]] std::string format(const Record& r);
5. When not to use it
| Kind | Example | Why |
|---|---|---|
| void | void log(...) | No return value |
| Side-effect primary | void flush() | Return not the contract |
| Conventionally ignored | printf (byte count) | Callers care about I/O, not the int |
6. Apply once on enum class / struct
When many functions return the same error or result type, mark the type once.
enum class [[nodiscard]] DbError { Ok, NotFound, ConnectionFailed };
[[nodiscard]] struct ParseResult {
int value;
bool ok;
};
7. Use C++20 reason strings on critical APIs
[[nodiscard("Store this handle or the resource may leak")]]
Handle acquireResource();
8. Decision flow
flowchart TD
A[Review function or type] --> B{Non-void return?}
B -->|No void| C{Unsafe if discarded?}
B -->|void| D[Do not use nodiscard]
C -->|Error/status| E[Use nodiscard]
C -->|Resource/RAII| E
C -->|optional/expected| E
C -->|Computed value| E
C -->|Side effects only / idiomatic discard| F[Do not use nodiscard]
E --> G[Apply nodiscard]
9. Team rules (example)
| Rule | Policy |
|---|---|
| Error/status returns | [[nodiscard]] required |
| Resource returns (ptr, handle) | [[nodiscard]] required |
| optional/expected | [[nodiscard]] required |
| RAII types | class [[nodiscard]] recommended |
| void | Do not apply |
| printf-style | Usually omit |
9. Production patterns
Pattern 1: Treat nodiscard violations as errors in CI
# GitHub Actions example
- name: Build with warnings as errors
run: |
cmake -B build -DCMAKE_CXX_FLAGS="-Werror=unused-result"
cmake --build build
Pattern 2: Clang-Tidy
# .clang-tidy
Checks: 'clang-analyzer-*,bugprone-*,performance-*,readability-*'
WarningsAsErrors: 'bugprone-*'
modernize-use-nodiscard suggests adding [[nodiscard]] where appropriate; the compiler still enforces discard of marked results.
Pattern 3: Consistent public API headers
// api.h
#pragma once
namespace mylib {
[[nodiscard]] bool init();
[[nodiscard]] std::unique_ptr<Session> createSession();
[[nodiscard]] std::optional<Config> loadConfig(const std::string& path);
}
Pattern 4: Error types marked once
// error_types.h
enum class [[nodiscard]] Status { Ok, Error, Timeout };
[[nodiscard]] struct Result {
int code;
std::string message;
};
Pattern 5: Gradual adoption on legacy code
// Phase 1: new APIs only
[[nodiscard]] std::optional<User> findUserNew(int id);
// Phase 2: add when refactoring
// bool init(); → [[nodiscard]] bool init();
Pattern 6: Macro for pre-C++17 (optional)
#if __cplusplus >= 201703L
#define NODISCARD [[nodiscard]]
#else
#define NODISCARD
#endif
NODISCARD bool init();
Pattern 7: Pair with documentation
/// @brief Run initialization. Returns false on failure.
/// @return true on success; callers must check the return value.
[[nodiscard("Unhandled initialization failure can crash later code paths")]]
bool init();
Pattern 8: Intentional discard in tests
[[nodiscard]] bool connect(const std::string& host);
void test_connection_failure() {
// Intention: invalid host → false; other asserts cover behavior
(void)connect("invalid.example.com");
assert(lastError() == ErrorCode::ConnectionFailed);
}
Pattern 9: Header-only libraries
Apply [[nodiscard]] across the public surface to reduce user mistakes.
// mylib/public/api.h
#pragma once
namespace mylib {
[[nodiscard]] bool initialize(const Config& cfg);
[[nodiscard]] std::unique_ptr<Session> createSession();
[[nodiscard]] std::optional<Data> fetch(const std::string& id);
[[nodiscard]] int getLastError() noexcept;
} // namespace mylib
Pattern 10: Compiler flags (MSVC, GCC, Clang)
| Compiler | Warning flag | Promote to error |
|---|---|---|
| GCC | -Wunused-result | -Werror=unused-result |
| Clang | -Wunused-result | -Werror=unused-result |
| MSVC | C4834 at /W3+ | /WX |
# GCC/Clang example
g++ -std=c++20 -Wunused-result -Werror=unused-result -o app main.cpp
Practical tips
While developing
- [Tip 1]: [Add project-specific guidance here.]
// example - [Tip 2]: [Add project-specific guidance here.]
// example - [Tip 3]: [Add project-specific guidance here.]
Debugging workflow
- [Approach 1]: [Describe your workflow.]
- [Approach 2]: [Describe your workflow.]
- [Approach 3]: [Describe your workflow.]
FAQ
Q. Does [[nodiscard]] add runtime overhead?
No. It is a compile-time attribute.
Q. What about MSVC?
MSVC supports [[nodiscard]]; warnings appear at /W3 and above. Use /WX to treat warnings as errors.
Q. Are std::make_unique and std::async already nodiscard?
Yes—many standard library functions have been marked [[nodiscard]] since C++17.
Q. Does nodiscard on a constructor always warn?
Only when the created object is a discarded-value expression—for example Guard(); as a statement, creating a temporary that is immediately destroyed.
10. Checklist
- Functions returning error codes use
[[nodiscard]] - Functions returning resources (
unique_ptr,shared_ptr, …) use[[nodiscard]] - Functions returning
optional/expecteduse[[nodiscard]] - Consider marking shared
enum class/ result structs once - Add reason strings (C++20) on critical APIs
- CI enables
-Wunused-resultor-Werror=unused-result - Do not mark
voidreturns
11. Summary
| Use case | Approach |
|---|---|
| Error code return | [[nodiscard]] |
Resource return (unique_ptr, …) | [[nodiscard]] |
| optional/expected | [[nodiscard]] |
| enum class / struct (shared type) | [[nodiscard]] on the type |
| C++20 reason | [[nodiscard("reason")]] |
| RAII (constructor misuse) | class [[nodiscard]] |
[[nodiscard]] means “do not discard this result.” Combined with CI and Clang-Tidy, it catches callers who skip checking errors, resources, and computed values before you reach production.
References:
- cppreference: nodiscard
- Clean C++ interfaces: const, noexcept, [[nodiscard]]
- Static analysis: Clang-Tidy and Cppcheck
Related reading (internal)
- Clean C++ interfaces: const, noexcept, [[nodiscard]]
- Static analysis in C++: Clang-Tidy & Cppcheck [#41-1]
- C++ in constrained environments: without exceptions and RTTI [#42-1]
Practical checklist
Things to verify when applying these ideas in real code.
Before writing code
- Is this the right tool for the problem?
- Will teammates understand and maintain it?
- Does it meet performance requirements?
While writing code
- Are compiler warnings clean?
- Are edge cases handled?
- Is error handling appropriate?
During code review
- Is intent obvious from the API?
- Are tests sufficient?
- Is important behavior documented?
Use this checklist to reduce mistakes and improve quality.
Keywords (search)
C++, nodiscard, ignored return value, error handling, RAII, clean code, C++17, C++20, static analysis
More related posts
- Full C++ series
- C++ Adapter pattern
- C++ ADL (argument-dependent lookup)
- C++ aggregate initialization