출시·고도화 중
C++ 안내서 · 5/6
C++에서 오류를 다루는 방법은 하나가 아닙니다. 예외, 반환 값으로 실패를 알리는 타입(std::optional, std::expected), 그리고 개발 중 버그를 잡는 단언(assertion)이 각자 쓰임새가 있습니다. 이 장에서는 이들을 언제 쓰는지 살펴보고, GoogleTest와 Catch2로 단위 테스트를 작성하는 방법을 소개합니다.
예외는 throw로 던지고 try/catch로 받습니다. 표준 라이브러리의 예외는 모두 std::exception을 상속하며, what()으로 메시지를 얻을 수 있습니다.
#include <iostream>
#include <stdexcept>
#include <string>
int parse_port(const std::string& text) {
int port = std::stoi(text); // 숫자가 아니면 std::invalid_argument
if (port < 1 || port > 65535) {
throw std::out_of_range("포트 범위를 벗어났습니다: " + text);
}
return port;
}
int main() {
try {
std::cout << parse_port("8080") << '\n';
std::cout << parse_port("70000") << '\n';
} catch (const std::out_of_range& e) {
std::cerr << "범위 오류: " << e.what() << '\n';
} catch (const std::exception& e) {
std::cerr << "기타 오류: " << e.what() << '\n';
}
}예외는 const 참조로 받습니다. 값으로 받으면 복사가 일어나고 파생 클래스 정보가 잘려 나갈 수 있습니다. catch 절은 위에서부터 차례로 비교하므로, 구체적인 타입을 먼저 적습니다.
예외가 던져지면 그 사이에 있던 지역 객체의 소멸자가 차례로 호출됩니다(스택 되감기). 그래서 자원을 객체가 관리하도록 만들면(6장의 RAII) 예외가 나도 자원이 새지 않습니다.
noexcept는 함수가 예외를 밖으로 던지지 않는다고 약속합니다. 이 약속을 어기고 예외가 빠져나가면 프로그램은 std::terminate로 종료됩니다. 표준 컨테이너는 원소의 이동 생성자가 noexcept일 때 재할당 과정에서 복사 대신 이동을 쓰므로, 이동 연산과 소멸자, swap 등에는 noexcept를 붙이는 것이 좋습니다.
#include <cstddef>
class Buffer {
public:
Buffer(Buffer&& other) noexcept; // 이동 생성자
Buffer& operator=(Buffer&& other) noexcept; // 이동 대입
~Buffer(); // 소멸자는 기본적으로 noexcept
std::size_t size() const noexcept { return size_; }
private:
std::size_t size_ = 0;
};값이 "없을 수도 있는" 것이 정상적인 상황이라면 예외보다 반환 타입으로 표현하는 편이 읽기 쉽습니다.
std::optional<T>(C++17, 헤더 <optional>): 값이 있거나 없음을 나타냅니다. 실패 이유는 담지 않습니다.std::expected<T, E>(C++23, 헤더 <expected>): 성공 값 T 또는 오류 값 E를 담습니다. C++23을 지원하는 컴파일러와 표준 라이브러리가 필요합니다.#include <map>
#include <optional>
#include <string>
std::optional<int> find_age(const std::map<std::string, int>& people,
const std::string& name) {
if (auto it = people.find(name); it != people.end()) {
return it->second;
}
return std::nullopt;
}
// if (auto age = find_age(people, "Kim")) { use(*age); }
// int a = find_age(people, "Park").value_or(0);#include <expected>
#include <string>
enum class ParseError { Empty, NotNumber };
std::expected<int, ParseError> to_int(const std::string& s) {
if (s.empty()) return std::unexpected(ParseError::Empty);
int value = 0;
for (char c : s) {
if (c < '0' || c > '9') return std::unexpected(ParseError::NotNumber);
value = value * 10 + (c - '0');
}
return value;
}
// auto r = to_int("42");
// if (r) { use(*r); } else if (r.error() == ParseError::Empty) { ... }<cassert>의 assert는 "절대 일어나면 안 되는 조건"을 확인합니다. 조건이 거짓이면 메시지를 출력하고 프로그램을 중단합니다. NDEBUG 매크로가 정의된 빌드(CMake의 Release 구성 등)에서는 검사 코드가 사라지므로, 사용자 입력 검증처럼 배포 후에도 필요한 검사에는 쓰지 않습니다. 컴파일 시간에 확인할 수 있는 조건은 static_assert를 씁니다.
#include <cassert>
#include <cstdint>
#include <vector>
static_assert(sizeof(std::int32_t) == 4, "int32_t 는 4바이트여야 합니다");
int last(const std::vector<int>& v) {
assert(!v.empty() && "빈 벡터에서 last 를 부르면 안 됩니다");
return v.back();
}GoogleTest는 가장 널리 쓰이는 C++ 테스트 프레임워크 중 하나입니다. CMake의 FetchContent로 가져와 테스트 실행 파일을 만들 수 있습니다. GIT_TAG에는 GoogleTest 저장소의 릴리스 태그를 적습니다.
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG <release-tag>
)
FetchContent_MakeAvailable(googletest)
enable_testing()
add_executable(math_test tests/math_test.cpp)
target_link_libraries(math_test PRIVATE mathx GTest::gtest_main)
include(GoogleTest)
gtest_discover_tests(math_test)#include <gtest/gtest.h>
#include "math_utils.hpp"
TEST(GcdTest, ReturnsGreatestCommonDivisor) {
EXPECT_EQ(mathx::gcd(12, 18), 6);
EXPECT_EQ(mathx::gcd(7, 5), 1);
}
TEST(GcdTest, ZeroIsHandled) {
ASSERT_EQ(mathx::gcd(9, 0), 9); // 실패하면 이 테스트를 즉시 멈춤
}EXPECT_*는 실패해도 테스트를 계속 진행하고, ASSERT_*는 그 자리에서 테스트를 멈춥니다. 빌드한 뒤 ctest --test-dir build로 모든 테스트를 실행합니다.
Catch2는 자연어에 가까운 테스트 이름과 REQUIRE/CHECK 매크로를 씁니다. Catch2 v3에서는 Catch2::Catch2WithMain 타깃에 링크하면 main을 직접 쓰지 않아도 됩니다.
#include <catch2/catch_test_macros.hpp>
#include "math_utils.hpp"
TEST_CASE("gcd 는 최대공약수를 돌려준다", "[math]") {
REQUIRE(mathx::gcd(12, 18) == 6);
CHECK(mathx::gcd(7, 5) == 1);
}const 참조로 받고, 구체적인 타입부터 catch합니다.noexcept를 붙입니다.std::optional(C++17), 실패 이유까지 돌려주려면 std::expected(C++23)를 씁니다.assert는 개발 중 버그 확인용이며, 컴파일 시간 조건은 static_assert로 확인합니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.
첫 댓글을 남겨 보세요.