
C++17과 C++20을 거치면서 표준 라이브러리에 실용적인 타입들이 많이 추가됐습니다. 외부 라이브러리 없이도 "값이 없을 수 있음", "여러 타입 중 하나", "파일 시스템 조작" 같은 걸 표준으로 처리할 수 있게 됐습니다.
이 편에서는 그 타입들을 한데 모아 정리합니다.
std::optional (C++17)
값이 있을 수도 없을 수도 있는 타입입니다. null 포인터나 -1 같은 sentinel 값 대신 의도를 명확하게 표현합니다.
#include <optional>
// 반환 타입으로 — "찾으면 값, 없으면 nullopt"
std::optional<int> find_index(const std::vector<int>& v, int target) {
for (int i = 0; i < v.size(); ++i)
if (v[i] == target) return i;
return std::nullopt;
}
auto idx = find_index(vec, 42);
// 확인 후 사용
if (idx) {
fmt::print("위치: {}\n", *idx);
}
if (idx.has_value()) { ... }
// 값 꺼내기
*idx; // 역참조 (없으면 UB)
idx.value(); // 없으면 std::bad_optional_access
idx.value_or(-1); // 없으면 기본값
// 수정
idx = 10;
idx.reset(); // nullopt로
idx.emplace(20); // 직접 생성
모나딕 연산 (C++23)
// and_then — 값이 있을 때만 함수 적용, optional 반환
auto result = find_index(vec, 42)
.and_then([&](int i) -> std::optional<std::string> {
if (i < names.size()) return names[i];
return std::nullopt;
});
// transform — 값이 있을 때만 변환
auto doubled = find_index(vec, 42)
.transform([](int i) { return i * 2; });
// or_else — 없을 때 대체 optional 제공
auto fallback = find_index(vec, 42)
.or_else([]() { return std::optional<int>{0}; });
std::variant (C++17)
여러 타입 중 정확히 하나를 담는 타입 안전 union입니다. union과 달리 어떤 타입이 들어있는지 런타임에 추적합니다.
#include <variant>
std::variant<int, double, std::string> v = 42;
// 타입 확인
std::holds_alternative<int>(v); // true
v.index(); // 0 (int의 인덱스)
// 값 꺼내기
std::get<int>(v); // 42
std::get<0>(v); // 인덱스로도 가능
std::get_if<int>(&v); // 포인터 반환, 틀리면 nullptr
// 변경
v = 3.14;
v = "hello";
v.emplace<std::string>("world");
// visit — 타입마다 다른 처리
std::visit([](auto&& val) {
using T = std::decay_t<decltype(val)>;
if constexpr (std::is_same_v<T, int>)
fmt::print("int: {}\n", val);
else if constexpr (std::is_same_v<T, double>)
fmt::print("double: {}\n", val);
else
fmt::print("string: {}\n", val);
}, v);
// 오버로드 패턴 (자주 쓰는 관용구)
template<class... Ts> struct overload : Ts... { using Ts::operator()...; };
std::visit(overload{
[](int i) { fmt::print("int {}\n", i); },
[](double d) { fmt::print("double {}\n", d); },
[](const std::string& s){ fmt::print("string {}\n", s); },
}, v);
에러 처리 패턴
using Result = std::variant<std::string, std::error_code>;
Result read_file(const std::string& path) {
std::ifstream f(path);
if (!f) return std::make_error_code(std::errc::no_such_file_or_directory);
return std::string{std::istreambuf_iterator<char>(f), {}};
}
auto r = read_file("config.txt");
std::visit(overload{
[](const std::string& s) { fmt::print("내용: {}\n", s); },
[](std::error_code ec) { fmt::print("에러: {}\n", ec.message()); },
}, r);
std::any (C++17)
타입 정보를 지우고 어떤 값이든 담을 수 있는 타입입니다. variant와 달리 담을 타입을 미리 지정하지 않아도 됩니다.
#include <any>
std::any a = 42;
a = 3.14;
a = std::string("hello");
// 타입 확인
a.type() == typeid(std::string);
a.has_value();
// 값 꺼내기
std::any_cast<std::string>(a); // 복사
std::any_cast<std::string&>(a); // 참조
std::any_cast<std::string*>(&a); // 포인터, 실패 시 nullptr
// 실패 시 예외
try {
std::any_cast<int>(a); // std::bad_any_cast
} catch (const std::bad_any_cast& e) { ... }
a.reset(); // 비우기
any는 타입 안전성이 런타임으로 미뤄지기 때문에, 타입을 미리 알 수 있는 상황에선 variant가 더 낫습니다. 플러그인 시스템, 설정 값 저장처럼 타입이 열려있는 경우에 씁니다.
std::string_view (C++17)
문자열의 비소유(non-owning) 뷰입니다. 복사 없이 문자열을 참조합니다.
#include <string_view>
// 함수 인수 — const std::string& 대신 쓰면 불필요한 복사 없음
void print(std::string_view sv) {
fmt::print("{} (len={})\n", sv, sv.size());
}
std::string s = "Hello, World";
print(s); // std::string → string_view (복사 없음)
print("literal"); // const char* → string_view (복사 없음)
print(s.substr(0, 5)); // 임시 string → string_view
// 부분 문자열 (복사 없음)
std::string_view sv = s;
sv.substr(7, 5); // "World" — 새 string 없이 뷰만
// 주요 연산
sv.starts_with("Hello"); // C++20
sv.ends_with("World"); // C++20
sv.find("World");
sv.remove_prefix(7); // sv 앞부분 제거 (원본 안 바뀜)
sv.remove_suffix(1);
// 주의: string_view는 수명이 원본에 묶임
std::string_view dangling() {
std::string s = "temp";
return s; // 위험! s가 소멸됨
}
std::span (C++20)
배열이나 연속 메모리의 비소유 뷰입니다. string_view의 배열 버전이라 생각하면 됩니다.
#include <span>
// 함수에서 배열/벡터/포인터 통일 처리
void process(std::span<int> data) {
for (auto& x : data) x *= 2;
fmt::print("size: {}\n", data.size());
}
std::vector<int> v = {1, 2, 3, 4, 5};
int arr[] = {1, 2, 3};
process(v); // vector
process(arr); // C 배열
process({arr, 3}); // 포인터 + 크기
// 부분 뷰 (복사 없음)
std::span<int> sp(v);
sp.subspan(1, 3); // [1, 3) 범위
sp.first(2); // 앞 2개
sp.last(2); // 뒤 2개
// 고정 크기 span
std::span<int, 3> fixed(arr); // 컴파일 타임 크기
// 읽기 전용
std::span<const int> ro(v);
std::filesystem (C++17)
파일과 디렉토리를 다루는 표준 라이브러리입니다.
#include <filesystem>
namespace fs = std::filesystem;
// 경로 조작
fs::path p = "/home/user/projects";
p / "myapp" / "src"; // 경로 이어붙이기
p.filename(); // "projects"
p.parent_path(); // "/home/user"
p.extension(); // 확장자
p.stem(); // 확장자 없는 파일명
p.string(); // std::string으로
p.is_absolute();
p.is_relative();
// 존재/타입 확인
fs::exists(p);
fs::is_regular_file(p);
fs::is_directory(p);
fs::is_symlink(p);
fs::file_size(p); // 바이트
// 디렉토리 조작
fs::create_directory(p);
fs::create_directories(p); // 중간 경로 포함
fs::remove(p); // 파일 또는 빈 디렉토리
fs::remove_all(p); // 디렉토리 통째로
fs::copy(src, dst);
fs::copy(src, dst, fs::copy_options::overwrite_existing);
fs::rename(old_p, new_p);
// 임시 경로
auto tmp = fs::temp_directory_path();
auto tmp_file = tmp / "myapp_XXXXXX";
// 현재 경로
fs::current_path();
fs::current_path(new_path); // 변경
// 순회
for (const auto& e : fs::directory_iterator(p)) {
fmt::print("{} {}\n",
e.path().filename().string(),
fs::is_directory(e) ? "[DIR]" : "");
}
// 재귀 순회
for (const auto& e : fs::recursive_directory_iterator(p)) {
if (e.path().extension() == ".cpp")
fmt::print("{}\n", e.path().string());
}
// 에러 처리 (예외 없이)
std::error_code ec;
fs::create_directory(p, ec);
if (ec) spdlog::error("디렉토리 생성 실패: {}", ec.message());
std::expected (C++23)
함수가 성공하면 값을, 실패하면 에러를 반환하는 타입입니다. optional과 비슷하지만 실패 이유도 담습니다. Rust의 Result<T, E>에 해당합니다.
#include <expected>
// 반환 타입
std::expected<int, std::string> parse(std::string_view s) {
try {
return std::stoi(std::string(s));
} catch (...) {
return std::unexpected("파싱 실패: " + std::string(s));
}
}
auto result = parse("42");
// 확인
if (result) {
fmt::print("값: {}\n", *result);
} else {
fmt::print("에러: {}\n", result.error());
}
result.value(); // 없으면 std::bad_expected_access
result.value_or(0); // 에러면 기본값
result.error(); // 에러 값 (에러 상태일 때만)
// 모나딕 연산 (C++23)
auto doubled = parse("21")
.and_then([](int n) -> std::expected<int, std::string> {
if (n < 0) return std::unexpected("음수 불가");
return n * 2;
})
.transform([](int n) { return fmt::format("결과: {}", n); })
.or_else([](const std::string& e) {
return std::expected<std::string, std::string>{"기본값"};
});
정리 표
| 타입 | 표준 | 핵심 용도 |
|---|---|---|
std::optional<T> |
C++17 | 값이 없을 수 있음 |
std::variant<Ts...> |
C++17 | 타입 안전 union |
std::any |
C++17 | 타입 소거 컨테이너 |
std::string_view |
C++17 | 문자열 비소유 뷰 |
std::span<T> |
C++20 | 배열 비소유 뷰 |
std::filesystem |
C++17 | 파일/디렉토리 조작 |
std::expected<T,E> |
C++23 | 값 또는 에러 반환 |
언제 뭘 쓰나
| 상황 | 선택 |
|---|---|
| 값이 없을 수 있는 반환 | optional |
| 성공/실패 + 에러 정보 | expected |
| 제한된 타입 집합 중 하나 | variant |
| 타입 미정 값 저장 | any |
| 문자열 인수로 받기 | string_view |
| 배열 인수로 받기 | span |
| 파일/경로 처리 | filesystem |
반응형
'블로그, 컴퓨터 > Cheatsheets' 카테고리의 다른 글
| C++ STL 정리 (2편) — 알고리즘, 이터레이터, 유틸리티 (0) | 2026.06.01 |
|---|---|
| C++ STL 정리 (1편) — 컨테이너 (0) | 2026.06.01 |
| GitHub Actions 정리 (2편) — 캐싱, Matrix, 재사용, 실전 패턴 (0) | 2026.05.31 |
| GitHub Actions 정리 (1편) — 개념, 워크플로우 구조, 트리거 (0) | 2026.05.31 |
| CMake 정리 (2편) — 서브디렉토리, 외부 라이브러리, 실용 패턴 (0) | 2026.05.30 |