C++ [[nodiscard]] 완벽 가이드 | 반환값 무시 방지·에러 코드·RAII·사유 메시지 [실전]

C++ [[nodiscard]] 완벽 가이드 | 반환값 무시 방지·에러 코드·RAII·사유 메시지 [실전]

이 글의 핵심

C++ [[nodiscard]] 완벽 가이드에 대한 실전 가이드입니다. 반환값 무시 방지·에러 코드·RAII·사유 메시지 [실전] 등을 예제와 함께 상세히 설명합니다.

들어가며: “반환값을 확인 안 해서 버그가 났어요”

실제 겪는 문제 시나리오

시나리오 1: 초기화 실패를 놓침
bool init()이 DB 연결·설정 로드 등에 실패하면 false를 반환합니다. 호출자가 init();만 쓰고 결과를 확인하지 않으면, 초기화가 실패했는데도 프로그램이 계속 진행되어 나중에 널 포인터 접근·크래시가 발생합니다. 원인: 반환값을 무시했기 때문입니다.

시나리오 2: unique_ptr 반환값 무시 → 메모리 누수
std::unique_ptr<Resource> create()가 리소스를 할당해 반환하는데, 호출자가 create();만 쓰고 받지 않습니다. unique_ptr이 즉시 소멸하면서 리소스는 해제되지만, 의도한 동작이 아니라면 “생성만 하고 사용하지 않는” 버그입니다. shared_ptr이나 커스텀 RAII를 반환할 때도 마찬가지로, 반환값 무시는 대부분 실수입니다.

시나리오 3: 에러 코드를 확인하지 않음
int connect(const char* host)가 0이면 성공, 음수면 에러 코드를 반환합니다. 호출자가 connect("db.example.com");만 쓰고 결과를 확인하지 않으면, 연결 실패 시에도 계속 진행해 잘못된 데이터를 쓰거나 크래시할 수 있습니다.

시나리오 4: optional/expected 반환값 무시
std::optional<User> findUser(int id)가 사용자를 찾지 못하면 std::nullopt를 반환합니다. 호출자가 findUser(42);만 쓰고 결과를 받지 않으면, “값이 있는지” 확인할 기회가 없어 널 참조나 잘못된 가정으로 이어질 수 있습니다.

시나리오 5: std::async 반환값 무시
std::async(std::launch::async, task)std::future를 반환합니다. 이 future를 받지 않으면 비동기 작업이 완료될 때까지 대기하지 않고, 데스트럭터에서 블로킹될 수 있습니다. 또한 결과를 받지 않으면 예외가 전파되지 않아 조용히 무시됩니다.

왜 nodiscard를 써야 하나요?
반환값 무시는 정적 분석으로 잡기 어렵고, 코드 리뷰에서도 놓치기 쉽습니다. [[nodiscard]]는 컴파일러가 컴파일 시점에 위반을 감지하게 하므로, 런타임 크래시·메모리 누수·보안 취약점을 사전에 막을 수 있습니다. 대안인 “함수 이름에 Check를 붙이기”, “문서에만 명시하기”는 강제력이 없어 실수로 무시하기 쉽습니다.

이 글에서 다루는 것:

  • [[nodiscard]]의 의미: “반환값을 무시하지 마라”
  • 적용 대상: 함수, enum class, 구조체, 생성자(C++20), 람다
  • 완전한 예제: 반환값 무시, 에러 코드, RAII 타입, nodiscard(“사유”), 생성자
  • 자주 하는 실수와 해결법
  • 프로덕션 패턴: CI·Clang-Tidy 연동

개념을 잡는 비유

C++에서 자주 쓰는 비유로, 템플릿은 붕어빵 틀, 스마트 포인터는 자동 청소 로봇, RAII는 자동문처럼 스코프를 나가며 자원을 정리한다고 기억해 두면 다른 글과도 연결하기 쉽습니다.


목차

  1. 문제 시나리오 상세
  2. [[nodiscard]]란 무엇인가
  3. 반환값 무시 예제
  4. 에러 코드 무시 예제
  5. RAII 타입과 nodiscard
  6. nodiscard(“사유”) (C++20)
  7. 자주 발생하는 에러와 해결법
  8. 베스트 프랙티스
  9. 프로덕션 패턴
  10. 체크리스트
  11. 정리

1. 문제 시나리오 상세

시나리오 A: init() 반환값 무시

// ❌ 잘못된 코드: 초기화 실패를 놓침
bool init() {
    if (!loadConfig()) return false;
    if (!connectDB()) return false;
    return true;
}

void main_loop() {
    init();  // 실패해도 무시됨!
    run();   // DB가 연결되지 않은 상태에서 실행 → 크래시
}

주의사항: void로 바꿔도 부수 효과만 있는 것처럼 보일 수 있어, 성공·실패를 값으로 돌려주는 API에는 [[nodiscard]]가 특히 유효합니다.

해결: [[nodiscard]]를 붙이면 반환값을 무시할 때 컴파일러가 경고합니다.

// ✅ 올바른 코드
[[nodiscard]] bool init() {
    if (!loadConfig()) return false;
    if (!connectDB()) return false;
    return true;
}

void main_loop() {
    if (!init()) {
        log_error("초기화 실패");
        return;
    }
    run();
}

시나리오 B: 에러 코드 무시

// ❌ 잘못된 코드: 에러 코드 확인 안 함
int openFile(const char* path) {
    FILE* f = fopen(path, "r");
    if (!f) return -errno;
    // ...
    return fd;
}

void process() {
    openFile("data.txt");  // 실패해도 무시
    // fd가 -1인데 파일을 사용하려 함 → 크래시
}

해결: [[nodiscard]]로 에러 코드 확인을 강제합니다.

// ✅ 올바른 코드
[[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("파일 열기 실패: %d", fd);
        return;
    }
    // fd 사용
}

시나리오 C: RAII 리소스 반환값 무시

// ❌ 잘못된 코드: unique_ptr 무시
std::unique_ptr<Connection> createConnection() {
    return std::make_unique<Connection>();
}

void setup() {
    createConnection();  // 생성만 하고 버림 → 의도한 동작인지 불명확
    // 연결을 사용하려면 어디선가 보관해야 하는데...
}

해결: [[nodiscard]]로 리소스 반환값 무시를 방지합니다.

// ✅ 올바른 코드
[[nodiscard]] std::unique_ptr<Connection> createConnection() {
    return std::make_unique<Connection>();
}

void setup() {
    auto conn = createConnection();
    if (conn) {
        conn->connect();
    }
}

시나리오 D: optional 반환값 무시

// ❌ 잘못된 코드: optional 무시
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);  // 결과를 받지 않음!
    // 사용자 정보를 표시하려면 결과가 필요한데...
}

해결: [[nodiscard]]로 optional 확인을 유도합니다.

// ✅ 올바른 코드
[[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("사용자를 찾을 수 없습니다");
    }
}

시나리오 E: std::async future 무시

// ❌ 잘못된 코드: future 무시
void run_async() {
    std::async(std::launch::async,  {
        throw std::runtime_error("비동기 오류");
    });
    // future를 받지 않음 → 예외가 조용히 무시됨
    // future 소멸 시 블로킹될 수 있음
}

해결: std::async는 이미 [[nodiscard]]가 붙어 있으나, 사용자 비동기 함수도 동일하게 적용합니다.

// ✅ 올바른 코드
[[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();  // 결과 사용
}

시나리오 F: 해시·암호화 결과 무시

// ❌ 잘못된 코드: 해시 결과를 검증에 사용하지 않음
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());  // 결과 무시!
    // 해시를 저장된 값과 비교해야 하는데, 비교할 값이 없음
    // → 보안 검증이 완전히 무시됨
}

해결: 보안·무결성 관련 함수는 반드시 [[nodiscard]]로 결과 확인을 강제합니다.

// ✅ 올바른 코드
[[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("무결성 검증 실패");
    }
}

시나리오 G: 생성자 반환값 무시 (RAII 위반)

// ❌ 잘못된 코드: 락을 잡고 바로 버림
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);  // 임시 객체 생성 → 즉시 소멸 → unlock 호출
    // 락이 풀린 상태에서 아래 코드 실행 → 데이터 레이스!
    doCriticalWork();
}

해결: C++20에서 class [[nodiscard]]로 생성된 객체를 무시할 때 경고합니다.

// ✅ 올바른 코드 (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);  // 변수에 저장 → 락 유지
    doCriticalWork();
}

2. [[nodiscard]]란 무엇인가

”반환값을 무시하지 마라”

[[nodiscard]]는 해당 함수·타입의 반환값을 무시하면 컴파일러가 경고(또는 오류)를 내도록 하는 속성입니다. C++17에서 도입되었고, C++20에서 사유 메시지생성자 적용이 추가되었습니다.

flowchart TB
    subgraph Call["호출부"]
        A[""nodiscard"] 함수 호출"] --> B{반환값 사용?}
        B -->|예| C[OK]
        B -->|아니오| D[컴파일 경고/오류]
        D --> E[버그 조기 발견]
    end

nodiscard 적용 전/후 비교:

상황nodiscard 없음nodiscard 있음
init(); (결과 무시)컴파일 통과, 런타임 크래시 가능컴파일 경고/오류
createResource(); (리소스 버림)메모리 누수·리소스 누수경고로 조기 발견
findUser(42); (optional 무시)널 참조·잘못된 가정경고로 조기 발견
sequenceDiagram
    participant Dev as 개발자
    participant Comp as 컴파일러
    participant Code as 코드

    Dev->>Code: init(); (반환값 무시)
    Code->>Comp: [[nodiscard]] bool init()
    Comp->>Comp: 반환값 사용 여부 검사
    Comp->>Dev: ⚠️ 경고: nodiscard 반환값 무시
    Dev->>Code: if (!init()) return;
    Comp->>Dev: ✅ 컴파일 성공

적용 가능한 대상

대상C++ 버전설명
함수C++17함수 선언에 붙임
enum classC++17해당 타입을 반환하는 모든 함수에 일괄 적용
구조체/클래스C++17해당 타입을 값으로 반환하는 함수에 적용
생성자C++20생성된 객체를 무시할 때 경고
람다C++17 [[nodiscard]] { return x; }
사유 메시지C++20[[nodiscard("메모리 누수 위험")]]

void 캐스팅으로 경고 억제

의도적으로 반환값을 무시할 때는 (void)expr로 명시할 수 있습니다.

[[nodiscard]] bool init();

void test() {
    (void)init();  // 의도적 무시, 경고 억제
}

3. 반환값 무시 예제

예제 1: bool 반환 함수

[[nodiscard]] bool loadConfig(const std::string& path) {
    std::ifstream f(path);
    if (!f) return false;
    // 파싱...
    return true;
}

void startup() {
    if (!loadConfig("config.json")) {
        std::cerr << "설정 로드 실패\n";
        std::exit(1);
    }
}

예제 2: 계산 결과 반환

[[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);  // 반환값 사용
    if (h != expectedHash) { /* ... */ }
}

예제 3: 팩토리 함수

[[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");
}

예제 4: getter (순수 조회)

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. 에러 코드 무시 예제

예제 1: enum class에 nodiscard (C++17)

[[nodiscard]] enum class 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) {  // 반환값 확인 필수
        return;
    }
    // f 사용
}

예제 2: Result 구조체 (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;
}

예제 3: std::expected 스타일 (C++23)

// C++23 std::expected 또는 커스텀 구현
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) {
    // ...
}

예제 4: errno 스타일 정수 반환

// 0 = 성공, 음수 = 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 타입과 nodiscard

예제 1: unique_ptr 반환

[[nodiscard]] std::unique_ptr<Buffer> createBuffer(size_t size) {
    return std::make_unique<Buffer>(size);
}

void use() {
    auto buf = createBuffer(1024);
    buf->write(data);
}

예제 2: shared_ptr 반환

[[nodiscard]] std::shared_ptr<Cache> getGlobalCache() {
    static auto cache = std::make_shared<Cache>();
    return cache;
}

예제 3: 커스텀 RAII 래퍼

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);  // lock을 무시하면 즉시 unlock → 위험
    // ...
}

예제 4: 파일 핸들 래퍼

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");  // f를 무시하면 파일이 열린 채로 소멸
    if (!f.isOpen()) return;
    // ...
}

예제 5: optional/expected 반환

[[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(“사유”) (C++20)

사유 메시지로 경고 명확화

C++20부터 [[nodiscard("문자열")]]경고 메시지에 표시할 사유를 지정할 수 있습니다.

[[nodiscard("반환된 unique_ptr을 무시하면 메모리 누수가 발생합니다")]]
std::unique_ptr<Resource> createResource() {
    return std::make_unique<Resource>();
}

[[nodiscard("에러 코드를 확인하지 않으면 초기화 실패를 놓칠 수 있습니다")]]
bool init() {
    return connect();
}

void bad() {
    createResource();  // 경고: "반환된 unique_ptr을 무시하면 메모리 누수가 발생합니다"
    init();            // 경고: "에러 코드를 확인하지 않으면 초기화 실패를 놓칠 수 있습니다"
}

생성자에 nodiscard (C++20)

생성자가 반환하는 객체를 무시하면 경고합니다. RAII 타입에서 “임시 객체로 생성만 하고 버리는” 실수를 막을 수 있습니다.

class [[nodiscard]] Guard {
public:
    Guard() { acquire(); }
    ~Guard() { release(); }
};

void bad() {
    Guard();  // 경고: Guard 객체를 무시함
}

void good() {
    Guard g;  // OK
}

전략적 가치 예제 (cppreference 스타일)

[[nodiscard("계산 결과를 사용해야 합니다")]] int strategic_value(int x, int y) {
    return x ^ y;
}

int main() {
    strategic_value(4, 2);       // 경고
    auto z = strategic_value(0, 0);  // OK
    return z;
}

생성자에 nodiscard 적용 (C++20) — 완전한 예제

C++20에서는 생성자에도 [[nodiscard]]를 적용할 수 있습니다. 클래스 전체에 class [[nodiscard]]를 붙이면, 해당 클래스의 생성자가 만든 객체를 무시할 때 경고가 발생합니다. RAII 타입에서 “임시 객체로 생성만 하고 곧바로 소멸”하는 실수를 막습니다.

// C++20: RAII 가드 클래스 — 객체를 변수에 저장하지 않으면 경고
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);  // ⚠️ 경고: MutexGuard 객체를 무시함
    // 생성 직후 소멸 → 락이 즉시 해제됨 → 아래 코드는 락 없이 실행
    doWork();  // 데이터 레이스 위험!
}

void good_example() {
    std::mutex mtx;
    MutexGuard guard(mtx);  // ✅ OK: 변수에 저장
    doWork();  // guard 소멸 시 unlock
}

생성자 nodiscard가 필요한 타입:

  • std::lock_guard, std::unique_lock 같은 락 가드
  • 파일 핸들, 소켓 핸들 래퍼
  • 트랜잭션 가드 (시작 시 BEGIN, 소멸 시 COMMIT/ROLLBACK)
  • 리소스 획득/해제 패턴의 모든 RAII 클래스

7. 자주 발생하는 에러와 해결법

문제 1: “반환값 무시 경고가 안 나와요”

증상: [[nodiscard]]를 붙였는데 경고가 나오지 않습니다.

원인: 컴파일러가 “권장” 수준으로만 경고를 내며, 기본적으로 비활성화되어 있을 수 있습니다.

해결법:

# GCC/Clang
-Wunused-result

# 경고를 오류로 처리
-Werror=unused-result

# CMake 예시
target_compile_options(myapp PRIVATE -Wunused-result -Werror=unused-result)

문제 2: “참조 반환에는 nodiscard가 적용 안 돼요”

증상: ErrorCode& foo()처럼 참조를 반환하는 함수에 enum에 nodiscard를 붙여도 경고가 안 납니다.

원인: nodiscard 타입이 값으로 반환될 때만 적용됩니다. 참조 반환은 “객체를 새로 만드는 것”이 아니므로 제외됩니다.

해결법:

// enum class에 nodiscard가 있어도
[[nodiscard]] enum class ErrorCode { Ok, Fail };

ErrorCode& getLastError();  // 참조 반환 → 경고 없음

void f() {
    getLastError();  // OK (참조이므로)
}

// 값 반환 시에만 경고
ErrorCode getError() { return ErrorCode::Ok; }
void g() {
    getError();  // 경고!
}

문제 3: “void 반환에 nodiscard를 붙였어요”

증상: void 함수에 [[nodiscard]]를 붙였는데 의미가 없습니다.

해결법: void에는 적용하지 않습니다. 반환값이 없으므로 무시할 것이 없습니다.

// ❌ 의미 없음
[[nodiscard]] void log(const std::string& msg);

// ✅ 반환값이 있는 함수에만
[[nodiscard]] bool tryLog(const std::string& msg);

문제 4: “의도적으로 무시하는데 경고가 나와요”

증상: 테스트나 특수한 경우에 반환값을 의도적으로 무시하는데 경고가 거슬립니다.

해결법:

[[nodiscard]] bool init();

void test_init_failure() {
    (void)init();  // 의도적 무시, 경고 억제
}

// 또는 std::ignore (tuple용이지만 패턴으로 사용)
#include <tuple>
void test2() {
    std::ignore = init();  // 일부 컴파일러에서 억제
}

문제 5: “상속 클래스에서 nodiscard 누락”

증상: 기본 클래스의 가상 함수에 nodiscard가 있는데, 오버라이드에서 빠뜨렸습니다.

해결법: 오버라이드에도 동일하게 붙입니다. 일관성을 위해 헤더에서 명시합니다.

struct Base {
    [[nodiscard]] virtual bool validate() const { return true; }
};

struct Derived : Base {
    [[nodiscard]] bool validate() const override { return check(); }
};

문제 6: “템플릿 함수에 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;
}

문제 7: “람다에 nodiscard”

증상: 람다가 값을 반환하는데 호출 결과를 무시할 수 있습니다.

해결법:

auto getValue =  [[nodiscard]] { return 42; };

void bad() {
    getValue();  // 경고
}

void good() {
    int x = getValue();  // OK
}

문제 8: “std::empty() 등 표준 함수와 혼동”

증상: std::vector::empty()[[nodiscard]]가 붙어 있어서 vec.empty();만 쓰면 경고가 납니다.

해결법: empty()의 반환값을 사용하세요. “비어 있는지” 확인만 할 때는 if (vec.empty())처럼 조건문에 넣습니다.

std::vector<int> vec;
if (vec.empty()) {  // OK: 조건문에서 사용
    // ...
}

vec.empty();  // 경고: 반환값 무시

문제 9: “매크로로 nodiscard 감싸면 C++17 미만에서 빌드 실패”

증상: #define NODISCARD [[nodiscard]]를 쓰고 C++14로 빌드하면 [[nodiscard]]를 인식하지 못해 오류가 납니다.

해결법: 전처리기로 C++ 버전을 분기합니다.

#if __cplusplus >= 201703L
    #define NODISCARD [[nodiscard]]
#else
    #define NODISCARD
#endif

NODISCARD bool init();  // C++17+: 경고, C++14: 무시

문제 10: “생성자에만 nodiscard를 붙이고 싶은데”

증상: 클래스 전체가 아닌 특정 생성자에만 nodiscard를 적용하고 싶습니다.

해결법: C++20에서는 생성자 선언에 직접 [[nodiscard]]를 붙일 수 있습니다.

class ResourceGuard {
public:
    [[nodiscard]] ResourceGuard();           // 이 생성자만 nodiscard
    ResourceGuard(const ResourceGuard&);     // 복사 생성자는 nodiscard 아님
};

문제 11: “반환 타입이 복잡한데 nodiscard 위치가 헷갈려요”

증상: std::optional<std::unique_ptr<T>> 같은 복잡한 반환 타입에서 [[nodiscard]]를 어디에 붙여야 할지 모르겠습니다.

해결법: 함수 선언 에 붙입니다. 반환 타입 뒤가 아닙니다.

// ✅ 올바른 위치
[[nodiscard]] std::optional<std::unique_ptr<Resource>> tryCreate();

// ❌ 잘못된 위치 (문법 오류)
std::optional<std::unique_ptr<Resource>> [[nodiscard]] tryCreate();

8. 베스트 프랙티스

1. 에러 코드·상태를 반환하는 함수

[[nodiscard]] bool init();
[[nodiscard]] int connect(const char* host);
[[nodiscard]] ErrorCode openFile(const std::string& path);

2. 리소스를 반환하는 함수

[[nodiscard]] std::unique_ptr<Resource> create();
[[nodiscard]] std::shared_ptr<Cache> getCache();
[[nodiscard]] std::expected<Data, Error> fetch();

3. optional/expected 반환

[[nodiscard]] std::optional<User> findUser(int id);
[[nodiscard]] std::expected<Config, std::string> loadConfig();

4. 계산 결과·순수 함수

[[nodiscard]] double compute(const Data& d);
[[nodiscard]] std::string format(const Record& r);

5. 붙이지 말아야 할 함수

유형예시이유
void 반환void log(...)반환값 없음
부수 효과만void flush()반환값이 부차적
관례적으로 무시printf (반환값=출력 바이트)호출 목적이 출력

6. enum class/구조체에 일괄 적용

에러 코드나 Result 타입을 여러 함수에서 반환할 때, 타입에 한 번만 붙이면 됩니다.

[[nodiscard]] enum class DbError { Ok, NotFound, ConnectionFailed };

[[nodiscard]] struct ParseResult {
    int value;
    bool ok;
};

7. C++20 사유 메시지 활용

중요한 API에는 사유를 붙여 경고 시 이유를 명확히 합니다.

[[nodiscard("이 핸들을 저장하지 않으면 리소스가 누수됩니다")]]
Handle acquireResource();

8. nodiscard 적용 여부 결정 플로우

flowchart TD
    A[함수/타입 검토] --> B{반환값이 있나?}
    B -->|아니오 void| C[적용 안 함]
    B -->|예| D{반환값 무시 시 위험?}
    D -->|에러 코드·상태| E[적용]
    D -->|리소스·RAII| E
    D -->|optional/expected| E
    D -->|계산 결과| E
    D -->|부수 효과만·관례적 무시| F[적용 안 함]
    E --> G["(nodiscard 붙이기"]]

9. 팀 규칙으로 정리하기

규칙설명
모든 에러 코드 반환 함수[[nodiscard]] 필수
리소스 반환 (ptr, handle)[[nodiscard]] 필수
optional/expected 반환[[nodiscard]] 필수
RAII 타입class [[nodiscard]] 권장
void 반환적용 금지
printf 등 관례적 무시적용 안 함

9. 프로덕션 패턴

패턴 1: CI에서 nodiscard 위반을 오류로 처리

# GitHub Actions 예시
- name: Build with warnings as errors
  run: |
    cmake -B build -DCMAKE_CXX_FLAGS="-Werror=unused-result"
    cmake --build build

패턴 2: Clang-Tidy로 nodiscard 점검

# .clang-tidy
Checks: 'clang-analyzer-*,bugprone-*,performance-*,readability-*'
WarningsAsErrors: 'bugprone-*'

Clang-Tidy의 modernize-use-nodiscard[[nodiscard]]를 붙이면 좋은 함수를 제안합니다. (반대로 “nodiscard 위반” 경고는 컴파일러가 담당)

패턴 3: API 헤더에 일괄 적용

// api.h
#pragma once

namespace mylib {
    [[nodiscard]] bool init();
    [[nodiscard]] std::unique_ptr<Session> createSession();
    [[nodiscard]] std::optional<Config> loadConfig(const std::string& path);
}

패턴 4: 에러 타입에 nodiscard

// error_types.h
[[nodiscard]] enum class Status { Ok, Error, Timeout };

[[nodiscard]] struct Result {
    int code;
    std::string message;
};

패턴 5: 레거시 코드 점진적 적용

// 1단계: 새 함수에만 적용
[[nodiscard]] std::optional<User> findUserNew(int id);

// 2단계: 리팩터링 시 기존 함수에 추가
// bool init();  →  [[nodiscard]] bool init();

패턴 6: 매크로로 C++17 미만 지원 (선택)

#if __cplusplus >= 201703L
    #define NODISCARD [[nodiscard]]
#else
    #define NODISCARD
#endif

NODISCARD bool init();

패턴 7: 문서화와 함께

/// @brief 초기화 수행. 실패 시 false 반환.
/// @return 성공 시 true. 반환값을 반드시 확인해야 함.
[[nodiscard("초기화 실패 시 처리하지 않으면 크래시할 수 있습니다")]]
bool init();

패턴 8: 테스트 코드에서 의도적 무시

테스트에서 “실패 케이스”를 검증할 때 반환값을 의도적으로 무시할 수 있습니다. 이때는 (void)로 명시하고 주석을 남깁니다.

[[nodiscard]] bool connect(const std::string& host);

void test_connection_failure() {
    // 의도: 잘못된 호스트로 연결 시 false 반환 확인
    // 반환값은 이 테스트에서 검증 대상이 아님 (다른 assert로 검증)
    (void)connect("invalid.example.com");
    assert(lastError() == ErrorCode::ConnectionFailed);
}

패턴 9: 헤더 전용 라이브러리에서 일괄 적용

공개 API 헤더에 nodiscard를 일괄 적용해 사용자 실수를 줄입니다.

// 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

패턴 10: MSVC·GCC·Clang 플래그 정리

컴파일러nodiscard 경고 플래그경고→오류
GCC-Wunused-result (기본 일부 활성)-Werror=unused-result
Clang-Wunused-result-Werror=unused-result
MSVC/W3 이상 (기본)/WX
# GCC/Clang 빌드 예시
g++ -std=c++20 -Wunused-result -Werror=unused-result -o app main.cpp

실무 팁

개발 시 주의사항

  1. [팁 1]: [설명]

    // 예시 코드
  2. [팁 2]: [설명]

    // 예시 코드
  3. [팁 3]: [설명]

디버깅 방법

  • [방법 1]: [설명]
  • [방법 2]: [설명]
  • [방법 3]: [설명]

FAQ

Q. [[nodiscard]]가 런타임 오버헤드를 주나요?
아닙니다. 컴파일 타임 속성이므로 런타임 비용이 없습니다.

Q. MSVC에서는 어떻게 하나요?
MSVC는 [[nodiscard]]를 지원하며, /W3 이상에서 경고가 나옵니다. /WX로 경고를 오류로 처리할 수 있습니다.

Q. std::make_unique, std::async는 이미 nodiscard인가요?
예. C++17부터 표준 라이브러리의 많은 함수에 [[nodiscard]]가 적용되어 있습니다.

Q. 생성자에 nodiscard를 붙이면 항상 경고가 나나요?
생성된 객체가 discarded-value expression에서 무시될 때만 경고됩니다. 예: Guard(); (임시 객체 생성 후 즉시 소멸)


10. 체크리스트

구현 시 확인할 항목:

  • 에러 코드를 반환하는 함수에 [[nodiscard]] 적용
  • 리소스를 반환하는 함수(unique_ptr, shared_ptr 등)에 적용
  • optional/expected 반환 함수에 적용
  • enum class/구조체에 일괄 적용 검토
  • C++20에서 중요한 API에 사유 메시지 추가
  • CI에서 -Wunused-result 또는 -Werror=unused-result 설정
  • void 반환 함수에는 적용하지 않음

11. 정리

용도사용
에러 코드 반환[[nodiscard]]
리소스 반환 (unique_ptr 등)[[nodiscard]]
optional/expected 반환[[nodiscard]]
enum class/구조체 (일괄)[[nodiscard]]
C++20 사유 메시지[[nodiscard("이유")]]
RAII 타입 (생성자)class [[nodiscard]]

[[nodiscard]]는 “반환값을 무시하지 마라”는 의미이며, 에러 코드·리소스·계산 결과를 반환하는 API에서 호출자가 결과를 확인하지 않는 실수를 컴파일 시점에 막습니다. CI와 Clang-Tidy와 함께 사용하면 코드 품질을 크게 향상시킬 수 있습니다.


참고 자료:


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • [C++ 클린 코드 기초: const, noexcept, ]로 인터페이스 의도 명확히 하기](/blog/cpp-series-38-1-clean-code-const-noexcept-nodiscard/)
  • C++ 정적 분석 도구 통합: Clang-Tidy와 Cppcheck로 코드 퀄리티 강제하기 [#41-1]
  • C++ 제약된 환경에서의 C++: Exception과 RTTI 없이 안전한 코드 짜기 [#42-1]

실전 체크리스트

실무에서 이 개념을 적용할 때 확인해야 할 사항입니다.

코드 작성 전

  • 이 기법이 현재 문제를 해결하는 최선의 방법인가?
  • 팀원들이 이 코드를 이해하고 유지보수할 수 있는가?
  • 성능 요구사항을 만족하는가?

코드 작성 중

  • 컴파일러 경고를 모두 해결했는가?
  • 엣지 케이스를 고려했는가?
  • 에러 처리가 적절한가?

코드 리뷰 시

  • 코드의 의도가 명확한가?
  • 테스트 케이스가 충분한가?
  • 문서화가 되어 있는가?

이 체크리스트를 활용하여 실수를 줄이고 코드 품질을 높이세요.


이 글에서 다루는 키워드 (관련 검색어)

C++, nodiscard, 반환값무시, 에러처리, RAII, 클린코드, C++17, C++20, 정적분석 등으로 검색하시면 이 글이 도움이 됩니다.


관련 글

  • C++ 시리즈 전체 보기
  • C++ Adapter Pattern 완벽 가이드 | 인터페이스 변환과 호환성
  • C++ ADL |
  • C++ Aggregate Initialization |