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

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

  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.
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, 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.
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

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)