본문으로 건너뛰기
Previous
Next
C++ [[nodiscard]] 완벽 가이드 | 반환값 무시 방지·에러 코드·RAII·사유 메시지 [실전]

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

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

이 글의 핵심

C++ [[nodiscard]] : 반환값 무시 방지·에러 코드·RAII·사유 메시지 […. 문제 시나리오 상세·nodiscard란 무엇인가.

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

실제 겪는 문제 시나리오

시나리오 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는 자동문처럼 스코프를 나가며 자원을 정리한다고 기억해 두면 다른 글과도 연결하기 쉽습니다.

실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.

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에서 사유 메시지생성자 적용이 추가되었습니다.

컴파일러가 nodiscard를 검사하는 내부 메커니즘:

소스 코드:

[[nodiscard]] bool init() {
    return connectDB();
}

int main() {
    init();  // 반환값 무시
}

컴파일러 AST (Abstract Syntax Tree) 생성:

FunctionDecl: init
  - return type: bool
  - attributes: [[nodiscard]]
  - body: { return connectDB(); }

CallExpr: init() in main
  - callee: init
  - return type: bool
  - parent statement: ExprStmt
  - value used: NO ← 중요!

Semantic Analysis 단계:

1. CallExpr 방문:
   
   if (call->getType() != void) {
       check if return value used
   }

2. 반환값 사용 여부 확인:
   
   코드:
   init();  // ExprStmt (Statement Expression)
   
   → 반환값이 다른 표현식에 사용 안 됨
   → value_used = false

3. nodiscard 속성 확인:
   
   FunctionDecl* func = call->getDirectCallee();
   if (func->hasAttr<WarnUnusedResultAttr>()) {
       if (!value_used) {
           emit_warning("ignoring return value of function declared with 'nodiscard'");
       }
   }

경고 생성:

warning: ignoring return value of function declared with 'nodiscard' [-Wunused-result]
    init();
    ^~~~~~

컴파일러별 구현:

GCC:
- -Wunused-result 플래그
- warn_unused_result 속성으로 인식
- Semantic Analysis에서 검사

Clang:
- -Wunused-result 플래그
- warn_unused_result 속성
- Sema::DiagnoseUnusedExprResult()

MSVC:
- C4834 경고
- _Check_return_ 속성과 유사

반환값 사용 판정:

사용됨:
auto x = init();         // 변수에 할당
if (init()) { }          // 조건문
return init();           // return 문
foo(init());             // 인자로 전달
x = init() ? 1 : 2;      // 3항 연산자
x && init();             // 논리 연산자

사용 안 됨:
init();                  // ExprStmt, 결과 버림
{ init(); }              // 블록 안에서 버림
for (...) { init(); }    // 루프 안에서 버림

void 캐스팅 (경고 억제):

(void)init();

AST:
CStyleCastExpr: (void)
  - child: CallExpr: init()
  - target type: void

컴파일러 인식:
→ 명시적으로 void로 캐스팅
→ "의도적 무시"로 판단
→ 경고 억제

타입 수준 nodiscard:

enum class [[nodiscard]] Status { Ok, Error };

Status connect() {
    return Status::Ok;
}

int main() {
    connect();  // 경고! Status 타입이 nodiscard
}

컴파일러 검사:
1. connect() 반환 타입: Status
2. Status 타입 정의 확인
3. [[nodiscard]] 속성 발견
4. 반환값 무시 → 경고

생성자 nodiscard (C++20):

class [[nodiscard]] Guard {
    Guard() { }
    ~Guard() { }
};

int main() {
    Guard();  // C++20: 경고!
}

AST:
CXXTemporaryObjectExpr: Guard()
  - type: Guard (nodiscard)
  - materialized: NO
  - parent: ExprStmt

검사:
1. 임시 객체 생성
2. Guard 타입이 nodiscard
3. 객체가 변수에 저장 안 됨
4. 즉시 소멸 → RAII 위반 가능성
5. 경고 발생

-Werror=unused-result 설정:

경고를 컴파일 오류로 승격:

g++ -Werror=unused-result main.cpp

결과:
error: ignoring return value of function declared with 'nodiscard'

CI/CD 적용:
→ nodiscard 위반 시 빌드 실패
→ 머지 블로킹
// 실행 예제
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++, nodiscard, 반환값무시, 에러처리, RAII, 클린코드, C++17, C++20, 정적분석 등으로 검색하시면 이 글이 도움이 됩니다.

관련 글

심화 부록: 구현·운영 관점

이 부록은 앞선 본문에서 다룬 주제(「C++ [[nodiscard]] 완벽 가이드 | 반환값 무시 방지·에러 코드·RAII·사유 메시지 [실전]」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.

내부 동작과 핵심 메커니즘

flowchart TD
  A[입력·요청·이벤트] --> B[파싱·검증·디코딩]
  B --> C[핵심 연산·상태 전이]
  C --> D[부작용: I/O·네트워크·동시성]
  D --> E[결과·관측·저장]
sequenceDiagram
  participant C as 클라이언트/호출자
  participant B as 경계(런타임·게이트웨이·프로세스)
  participant D as 의존성(API·DB·큐·파일)
  C->>B: 요청/이벤트
  B->>D: 조회·쓰기·RPC
  D-->>B: 지연·부분 실패·재시도 가능
  B-->>C: 응답 또는 오류(코드·상관 ID)
  • 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
  • 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
  • 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
  • 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.

프로덕션 운영 패턴

영역운영 관점 질문
관측성요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가
안전성입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가
신뢰성재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가
성능캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가
배포롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가
용량피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가

스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.

확장 예시: 엔드투엔드 미니 시나리오

앞선 본문 주제(「C++ [[nodiscard]] 완벽 가이드 | 반환값 무시 방지·에러 코드·RAII·사유 메시지 [실전]」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  1. 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
  2. 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
  3. 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
  4. 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
  5. 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
  ctx = newCorrelationId()
  validated = validateSchema(request)
  authorize(validated, ctx)
  result = domainCore(validated)
  persistOrEmit(result, idempotentKey)
  recordMetrics(ctx, latency, outcome)
  return result

문제 해결(Troubleshooting)

증상가능 원인조치
간헐적 실패레이스, 타임아웃, 외부 의존성, DNS최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검
성능 저하N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거
메모리 증가캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납상한·TTL·힙/FD 스냅샷 비교
빌드·배포만 실패환경 변수, 권한, 플랫폼 차이, lockfileCI 로그와 로컬 diff, 런타임·이미지 버전 핀
설정 불일치프로필·시크릿·기본값, 리전스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화
데이터 불일치비멱등 재시도, 부분 쓰기, 캐시 무효화 누락멱등 키·아웃박스·트랜잭션 경계 재검토

권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.

배포 전에는 git addgit commitgit pushnpm run deploy 순서를 권장합니다.