C++ Clean Code Basics: Express Intent with const, noexcept, and [[nodiscard]]
이 글의 핵심
A practical guide to making C++ interfaces explicit with const, noexcept, and [[nodiscard]].
Introduction: Encode intent in the code
“Does this function have side effects? Can it throw?”
Series 31 covered syntax and patterns; series 38 is about how you arrange and connect them. The first step is making intent explicit in interfaces.
const, noexcept, and [[nodiscard]] tell the compiler—and the next reader—what a function does not do and what it guarantees. That catches ignored return values and exception-safety mistakes at compile time.
This article covers:
- Const correctness: express read-only and “no mutation” to prevent misuse
- noexcept: contract of no exceptions—interaction with move, optimization, and RAII
[[nodiscard]]: warn when return values are ignored to prevent API misuse
Problem scenarios: When interfaces are vague
Scenario 1: A Config passed without const gets mutated
Problem: You called process(const Config& cfg) but an internal call mutates cfg, causing subtle bugs. Without const, there is no guarantee of read-only use, so callers cannot safely pass by reference.
Fix: Take const Config& and make every callee a const member function so the compiler blocks mutation attempts.
Scenario 2: vector::resize copies instead of moving
Problem: A custom type in std::vector triggers copies on resize/push_back because the move constructor is not noexcept. The standard library may prefer copy when move could throw.
Fix: Mark move constructor, move assignment, and destructor noexcept where appropriate so std::vector can use moves.
Scenario 3: Ignoring init() hides initialization failure
Problem: bool init() returns false on failure, but callers write init(); and never check—production runs with failed initialization.
Fix: Declare [[nodiscard]] bool init() so ignoring the return value triggers a warning or error.
Scenario 4: Ignoring create()’s unique_ptr leaks memory
Problem: Ignoring std::unique_ptr<Resource> create() leaves allocated resources unreleased.
Fix: [[nodiscard]] std::unique_ptr<Resource> create();
Table of contents
- Const correctness
- noexcept
[[nodiscard]]- Common errors and fixes
- Production patterns
- Complete clean-code example
- Performance notes
- Summary
1. Const correctness
Marking read-only intent
- A const member function promises not to change the logical state of the object. The compiler rejects non-
constcalls onconstobjects; it also hints thread-safety when discussing read-only access. - const references/pointers mean this function does not modify the argument.
flowchart TD
subgraph const_usage["Where const helps"]
A[const member functions] --> A1["No object state change"]
B[const& parameters] --> B1["No argument mutation"]
C[const returns] --> C1["Non-modifiable result"]
end
A1 --> D[Compiler-enforced]
B1 --> D
C1 --> D
If get is const, process can call get on const Config& but not set.
class Config {
public:
std::string get(const std::string& key) const;
void set(const std::string& key, std::string value);
};
void process(const Config& cfg) {
auto v = cfg.get("timeout");
// cfg.set("x", "y"); // error: non-const call on const object
}
Overloading const member functions
Overload the same name as const/non-const: const objects invoke the const version; non-const invoke the other. Useful for operator[] separating read vs write.
class StringBuffer {
std::string data_;
public:
char& operator[](size_t i) { return data_[i]; }
const char& operator[](size_t i) const { return data_[i]; }
};
void use(const StringBuffer& buf) {
char c = buf[0];
// buf[0] = 'x'; // error
}
mutable: logical const vs physical const
Use mutable sparingly for members that are logically const but need physical updates (cache, mutex, stats).
class CachedLookup {
std::map<std::string, int> cache_;
mutable std::mutex mutex_;
public:
int get(const std::string& key) const {
std::lock_guard<std::mutex> lock(mutex_);
auto it = cache_.find(key);
return it != cache_.end() ? it->second : -1;
}
};
Caution: Overusing mutable undermines the “const means safe to call from readers” story—reserve it for caches, locks, logging, etc.
const parameters: avoid copies + forbid mutation
Prefer const& for large inputs you do not modify. Use const T* for “won’t modify what is pointed to.”
void process(const std::string& name);
void parse(const char* data, size_t len);
const return types
Return const T& or const T* when callers must not mutate the result.
class Container {
std::vector<int> data_;
public:
const std::vector<int>& getData() const { return data_; }
};
2. noexcept
Contract: this function does not throw
- noexcept states this function will not throw. For move operations,
noexcepthelps standard containers prefer move over copy when reallocating. - Omitted or noexcept(false) means exceptions are possible. Destructors and move operations should usually be noexcept-friendly.
flowchart LR
subgraph no_noexcept["Without noexcept"]
A1["vector resize"] --> A2{move ctor}
A2 -->|may throw| A3[copy chosen]
end
subgraph with_noexcept["With noexcept"]
B1["vector resize"] --> B2{move ctor}
B2 -->|noexcept| B3[move chosen]
end
class Buffer {
public:
Buffer(size_t size) : data_(new char[size]), size_(size) {}
Buffer(Buffer&& other) noexcept
: data_(std::exchange(other.data_, nullptr)),
size_(std::exchange(other.size_, 0)) {}
~Buffer() noexcept { delete[] data_; }
private:
char* data_;
size_t size_;
};
Why noexcept destructor? Throwing from destructors during stack unwinding can call std::terminate. Design destructors not to throw and document with noexcept.
swap and basic operations
swap typically does not throw when swapping handles/pointers—mark it noexcept to help generic algorithms.
Conditional noexcept: noexcept(expr)
template<typename T>
class Optional {
T value_;
bool has_value_;
public:
Optional(Optional&& other) noexcept(std::is_nothrow_move_constructible_v<T>)
: value_(std::move(other.value_))
, has_value_(other.has_value_) {
other.has_value_ = false;
}
};
noexcept and std::vector
On reallocation, if the move constructor is noexcept, vector uses move; otherwise it may copy to preserve the strong exception guarantee.
3. [[nodiscard]]
Prevent ignored return values
Functions marked [[nodiscard]] trigger diagnostics if the return value is unused—ideal for error codes, new resources, and computed results.
flowchart TD
A["[[nodiscard]] call"] --> B{Return value used?}
B -->|Yes| C[OK]
B -->|No| D[Warning or error]
D --> E[Catch bugs early]
[[nodiscard]] bool init();
[[nodiscard]] std::unique_ptr<Resource> create();
C++20: [[nodiscard]] on enum class / types
You can mark types so all functions returning them inherit the attribute—useful for error enums and result types.
4. Common errors and fixes
Error 1: calling non-const members on const X&
Symptom: passing 'const X' as 'this' argument discards qualifiers
Fix: add const to getters and other non-mutating members.
Error 2: mutating members inside const members
Fix: use mutable for caches/locks that do not affect logical constness.
Error 3: vector chooses copy because move isn’t noexcept
Fix: Buffer(Buffer&&) noexcept with std::exchange for pointer members.
Error 4: ignoring init() / factory returns
Fix: [[nodiscard]] on bool and std::unique_ptr factories.
5. Production patterns
RAII handles with noexcept destructor/moves; factories marked [[nodiscard]]; read-only repositories use const methods and const& parameters; swap is noexcept.
6. Complete example
#include <memory>
#include <string>
#include <expected>
enum class DbError { NotConnected, QueryFailed, Timeout };
class Database {
std::unique_ptr<Connection> conn_;
public:
[[nodiscard]] std::expected<QueryResult, DbError>
query(const std::string& sql) const noexcept {
if (!conn_) return std::unexpected(DbError::NotConnected);
return conn_->execute(sql);
}
void close() noexcept {
if (conn_) conn_->disconnect();
}
};
7. Performance notes
| Priority | Target | Why |
|---|---|---|
| 1 | Destructor noexcept | Exception-safety baseline |
| 2 | Move noexcept | vector reallocation |
| 3 | [[nodiscard]] on errors/resources | Prevent silent failures |
8. Summary
| Tool | Role |
|---|---|
| const | Read-only intent |
| noexcept | No-throw contract for moves/RAII |
| [[nodiscard]] | Force callers to handle returns |
References
Related posts (internal links)
- C++ const correctness
- C++ [[nodiscard]]
- English const guide
Keywords
C++ const correctness, noexcept, nodiscard, clean code, API design, exception safety
FAQ
Q. When do I use this in practice?
A. When designing APIs and in code review; roll out gradually on legacy code.
Q. Does const affect performance?
A. const itself is free; const& avoids copies for large objects.
Q. Next steps?
A. Polymorphism & variant (#38-2) — Series index
Previous: C++23 highlights (#37-1)