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는 자동문처럼 스코프를 나가며 자원을 정리한다고 기억해 두면 다른 글과도 연결하기 쉽습니다.
목차
- 문제 시나리오 상세
- [[nodiscard]]란 무엇인가
- 반환값 무시 예제
- 에러 코드 무시 예제
- RAII 타입과 nodiscard
- nodiscard(“사유”) (C++20)
- 자주 발생하는 에러와 해결법
- 베스트 프랙티스
- 프로덕션 패턴
- 체크리스트
- 정리
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 class | C++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]: [설명]
// 예시 코드 -
[팁 2]: [설명]
// 예시 코드 -
[팁 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와 함께 사용하면 코드 품질을 크게 향상시킬 수 있습니다.
참고 자료:
- cppreference: nodiscard
- C++ 클린 코드 기초: const, noexcept, [[nodiscard]]
- 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 |