반환값의 침묵은 금이 아니다: C++ nodiscard 속성의 철학과 실전 가이드
여러분은 혹시 중요한 메시지를 읽지 않고 휴지통에 버린 적이 있으신가요? 프로그래밍의 세계에서 [[nodiscard]]는 바로 그런 실수를 방지하기 위해 탄생한 일종의 안전장치입니다.

코드 한 줄이 시스템 전체의 운명을 결정짓는 순간이 있습니다. 우리는 종종 함수를 호출하고 그 결과가 당연히 성공했을 것이라 믿으며 다음 단계로 넘어가곤 하죠. 하지만 그 믿음이 깨지는 순간, 프로그램은 소리 없이 무너집니다. 여러분은 혹시 중요한 메시지를 읽지 않고 휴지통에 버린 적이 있으신가요? 프로그래밍의 세계에서 [[nodiscard]]는 바로 그런 실수를 방지하기 위해 탄생한 일종의 안전장치입니다.
이 기능은 단순히 컴파일러가 잔소리를 하게 만드는 도구가 아닙니다. 그 너머에는 소프트웨어 설계자가 호출자에게 전달하는 명확한 의도와 배려가 담겨 있습니다. 오늘은 현대 C++의 정수라고 할 수 있는 이 속성을 아주 깊게 파헤쳐 보려고 합니다.
1. [[nodiscard]]가 세상에 나오게 된 배경
프로그래밍을 하다 보면 반환값이 프로그램의 생사를 결정하는 경우가 정말 많습니다. 하지만 인간은 망각의 동물이죠. 감이 잘 오시지 않을 수 있어요. 구체적인 사례를 통해 살펴볼까요?
가장 흔한 문제는 메모리 누수입니다. 새로운 메모리를 할당해서 그 주소를 반환했는데, 호출자가 이를 받지 않으면 어떻게 될까요? 그 메모리는 시스템 내에서 영원히 길을 잃은 미아가 됩니다. 에러 처리도 마찬가지입니다. 파일 열기에 실패했다는 신호를 보냈음에도 이를 무시하고 데이터를 쓰려고 하면 프로그램은 즉시 비정상 종료라는 파국을 맞이하게 되죠.
또한 성능적인 측면에서도 손해입니다. 아무런 상태도 바꾸지 않고 오직 결과값만 계산해서 돌려주는 함수인데, 그 결과를 쓰지 않는다면 CPU 자원을 낭비한 셈이 됩니다. 이러한 비효율과 위험을 막기 위해 C++17에서 [[nodiscard]]라는 이름의 속성이 공식적으로 도입되었습니다.
2. 실전에서 마주하는 [[nodiscard]]의 세 가지 얼굴
자원 관리의 파수꾼
메모리를 할당하거나 락을 획득하는 함수에서 결과를 무시하는 행위는 치명적입니다. 아래 코드를 함께 보시죠.
[[nodiscard]] int* allocate_memory() {
return new int[100];
}
int main() {
allocate_memory(); // 경고! 할당된 주소를 받지 않았으므로 메모리 누수가 발생합니다.
}
이처럼 컴파일러는 우리에게 반환값을 반드시 어딘가에 저장하라고 경고를 보냅니다. 이를 통해 우리는 잠재적인 버그를 사전에 차단할 수 있습니다.
에러 코드의 엄격한 감시자
중요한 작업의 성공 여부를 반환할 때도 이 속성은 빛을 발합니다.
enum class ErrorCode { Success, Fail };
[[nodiscard]] ErrorCode save_data() {
// 데이터 저장 로직 수행
return ErrorCode::Fail;
}
int main() {
save_data(); // 경고! 데이터 저장 실패 가능성을 확인하지 않았습니다.
}
데이터가 제대로 저장되지 않았는데 다음 로직으로 넘어가는 것은 위험한 일입니다. [[nodiscard]]는 호출자가 최소한 에러 여부는 확인하도록 강제하는 역할을 합니다.
순수 함수의 가치 증명
std::empty()나 std::vector::size() 같은 함수들은 객체의 내부 상태를 변경하지 않습니다. 오로지 정보만 제공할 뿐이죠. 만약 이런 함수의 반환값을 쓰지 않는다면, 호출 자체가 무의미한 경우가 많습니다.
std::vector<int> v;
v.empty(); // 경고! (C++20부터 적용) 비었는지 확인만 하고 결과는 버려졌습니다.
혹시 여러분은 v.clear()와 혼동하여 v.empty()를 사용하진 않으셨나요? 컴파일러는 이 속성을 통해 우리의 숨겨진 의도까지 질문하게 됩니다.
3. C++20에서 더 친절해진 경고 메시지
C++17의 [[nodiscard]]는 단순히 경고만 띄웠을 뿐, 왜 무시하면 안 되는지에 대한 설명은 부족했습니다. 하지만 C++20부터는 경고의 이유를 직접 적어줄 수 있게 되었습니다.
[[nodiscard("메모리 누수가 발생하므로 반드시 스마트 포인터에 담으세요.")]]
void* heavy_allocation() { return malloc(1024); }
이제 동료 개발자는 컴파일러의 경고창에서 우리가 정성스럽게 작성한 안내 문구를 보게 될 것입니다. 협업의 관점에서 볼 때 이는 매우 강력한 소통 도구가 됩니다.
4. 생성자에도 붙일 수 있다는 사실을 알고 계셨나요?
임시 객체를 생성하고 바로 소멸시키는 실수는 RAII 패턴을 사용할 때 자주 발생합니다. C++20부터는 생성자에도 이 속성을 부여할 수 있습니다.
struct ScopedLock {
[[nodiscard]] ScopedLock() { /* 뮤텍스 잠금 */ }
~ScopedLock() { /* 뮤텍스 해제 */ }
};
int main() {
ScopedLock(); // 경고! 객체가 생성되자마자 파괴되어 락이 즉시 해제됩니다.
// ScopedLock lock; 이라고 작성해야 의도대로 동작합니다.
}
이 기능을 사용하면 락이 걸리지 않은 채 임계 구역에 진입하는 무서운 상황을 예방할 수 있습니다.
5. 명시적으로 무시해야 할 때의 탈출구
물론 세상을 살다 보면 모든 규칙을 다 지킬 수 없는 순간이 옵니다. 반환값이 중요하지 않다는 사실을 이미 알고 있고, 의도적으로 무시하고 싶을 때는 어떻게 해야 할까요? 그럴 때는 (void) 캐스팅이라는 명시적인 방법을 사용하면 됩니다.
(void)save_data(); // 컴파일러에게 "내가 알고 무시하는 거야"라고 선언합니다.
이렇게 하면 컴파일러는 더 이상 우리를 방해하지 않습니다. 하지만 이는 어디까지나 최후의 수단이어야 하겠죠.
결국 [[nodiscard]]는 단순한 문법이 아닙니다. 내 함수를 사용하는 여러분의 동료, 혹은 미래의 나 자신이 안전하게 코딩할 수 있도록 가이드라인을 제공하는 배려의 산물입니다. 현대적인 라이브러리 곳곳에 이 속성이 붙어 있는 이유는 바로 이 때문입니다.
[[nodiscard]]에 대한 궁금증이 좀 풀리셨나요? 이제는 이 속성을 활용해 더 견고한 코드를 작성해 보시는 건 어떨까요?