[2026] C++ Clean Code Basics: Express Intent with const, noexcept, and [[nodiscard]]
이 글의 핵심
Use const correctness, noexcept, and [[nodiscard]] in C++ APIs so interfaces state what they guarantee. Practical patterns, examples, and interview-ready explanations for safer, clearer code.
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. 아래 코드는 mermaid를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 실행 예제
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.
아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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.
아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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). 아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 타입 정의
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.
아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.
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. 아래 코드는 mermaid를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 실행 예제
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
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 타입 정의
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)
아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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. 아래 코드는 mermaid를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
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
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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)
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)