[2026] C++ Clean Code Basics: Express Intent with const, noexcept, and [[nodiscard]]

[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

  1. Const correctness
  2. noexcept
  3. [[nodiscard]]
  4. Common errors and fixes
  5. Production patterns
  6. Complete clean-code example
  7. Performance notes
  8. 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-const calls on const objects; 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, noexcept helps 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

PriorityTargetWhy
1Destructor noexceptException-safety baseline
2Move noexceptvector reallocation
3[[nodiscard]] on errors/resourcesPrevent silent failures

8. Summary

ToolRole
constRead-only intent
noexceptNo-throw contract for moves/RAII
[[nodiscard]]Force callers to handle returns

References


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)

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3