Skip to content

C++ 치트시트

OOP, 제네릭, 저수준 제어를 갖춘 범용 언어.

01

시작하기

Hello World

모든 C++ 프로그램은 main()에서 시작합니다. <iostream>은 std::cout(표준 출력)과 std::cin(표준 입력)을 제공합니다. std:: 접두어는 표준 네임스페이스를 참조; using namespace std;로 피할 수 있지만 전역 네임스페이스를 오염시키므로 헤더에서는 권장하지 않습니다.

cpp
#include <iostream>

int main() {
    std::cout << "Hello, World!" << std::endl;
    return 0;
}

변수 & auto

auto는 초기화자에서 타입을 추론합니다(C++11). 타입이 명확하거나 장황할 때(반복자) auto를 사용하세요. const는 값을 불변으로 만듭니다; constexpr은 컴파일 타임에 평가되어 바이너리에 임베드된 진정한 상수입니다.

cpp
int age = 30;
double pi = 3.14159;
char grade = 'A';
bool is_dev = true;
std::string name = "Alice";
auto x = 42;          // int
auto y = 3.14;        // double
const double TAX = 0.08;

입력 & 출력

std::getline은 공백을 포함한 전체 줄을 읽고, std::cin >>는 공백에서 중지합니다. 둘을 혼합하면 버퍼에 줄바꿈이 남음; getline과 >> 사이에 std::cin.ignore()를 호출하여 버리세요.

cpp
#include <iostream>
#include <string>

int main() {
    std::string name;
    int age;
    std::cout << "Name: ";
    std::getline(std::cin, name);
    std::cout << "Age: ";
    std::cin >> age;
    std::cout << "Hi " << name << ", " << age << "\n";
}

참조

참조는 초기화되어야 하고 다른 객체로 재할당할 수 없는 별칭입니다. 참조로 전달은 복사를 피하고 호출자의 변수를 수정할 수 있게 합니다. 비싼 복사를 피하기 위해 읽기 전용 매개변수에는 const T&를 사용하세요.

cpp
int x = 10;
int &ref = x;   // alias for x
ref = 20;
std::cout << x;   // 20

void increment(int &n) { n++; }
int a = 5;
increment(a);   // a is now 6

타입 변환

의도가 명확하고 컴파일 타임에 검사되므로 C 스타일 캐스트보다 static_cast를 선호하세요. std::stoi, std::stod는 문자열을 숫자로 변환; std::to_string은 역방향. 잘못된 입력에 std::out_of_range를 주의하세요.

cpp
double d = 3.99;
int i = (int)d;               // C-style: 3
int j = static_cast<int>(d);  // C++: 3

int n = 65;
char c = static_cast<char>(n);  // 'A'

// string <-> number
int num = std::stoi("42");
std::string s = std::to_string(3.14);
02

문자열

std::string 기본

std::string은 자체 메모리를 관리하고 필요에 따라 성장합니다. C char 배열과 달리 길이를 수동으로 관리하지 않습니다. .find()는 부분 문자열을 찾지 못하면 std::string::npos(매우 큰 값)를 반환하므로 항상 npos와 비교하세요.

cpp
#include <string>
std::string s = "Hello";
std::cout << s.length();      // 5
std::cout << s[0];            // H
s += ", World!";
std::cout << s;               // Hello, World!
std::cout << s.substr(0, 3);  // Hel
std::cout << s.find("World"); // 7

비교 & 검색

비교는 사전순(사전 순서)입니다. .find()는 앞으로 검색, .rfind()는 뒤로 검색. 둘 다 찾지 못하면 std::string::npos를 반환하므로 결과를 boolean으로 취급하지 말고 항상 npos와 비교하세요.

cpp
std::string a = "apple", b = "banana";
if (a == b) { /* equal */ }
if (a < b) { /* apple comes before banana */ }
if (a.find("pp") != std::string::npos) {
    std::cout << "found\n";
}
size_t pos = a.rfind("p");  // last occurrence

Stringstream

stringstream은 문자열과 타입이 지정된 값을 연결하여 형식화된 문자열 구축(버퍼처럼)이나 공백으로 구분된 토큰 파싱에 유용. 직접 작업보다 느리지만 직렬화와 역직렬화에 매우 유연.

cpp
#include <sstream>
// Build a string
std::ostringstream oss;
oss << "Name=" << "Alice" << "&Age=" << 30;
std::string result = oss.str();

// Parse tokens
std::istringstream iss("10 20 30");
int a, b, c;
iss >> a >> b >> c;   // a=10, b=20, c=30

Raw 문자열 & 멀티라인

Raw 문자열 리터럴 R"(...)"는 백슬래시와 따옴표를 리터럴로 처리하여 정규식 패턴, Windows 파일 경로, JSON/XML 템플릿에 이상적. 괄호 안의 구분자는 임의, 예: R"x(...)x"는 내부에 )를 허용.

cpp
std::string raw = R"(C:\Users\name\file.txt)";
// No need to escape backslashes

std::string json = R"({
  "name": "Alice",
  "age": 30
})";

char 배열 vs std::string

C 스타일 char 배열은 수동 크기 관리가 필요하고 오류 발생이 쉬움(버퍼 오버플로우). std::string을 선호; const char*를 예상하는 C API와 인터페이스할 때 .c_str() 사용. c_str()은 문자열이 살아있고 수정되지 않은 동안에만 유효.

cpp
char cstr[] = "Hello";   // null-terminated, size 6
std::string cppstr = "Hello";

#include <cstring>
std::cout << strlen(cstr);      // 5
std::cout << cppstr.length();   // 5

std::string from_c = cstr;          // C -> C++
const char* to_c = cppstr.c_str();  // C++ -> C
03

숫자 & 수학

정수 & 부동 소수점 타입

플랫폼 간에 정확한 크기가 중요할 때 <cstdint> 고정 너비 타입(int32_t, int64_t)을 사용. ' 숫자 구분자(C++14)는 큰 숫자의 가독성을 향상. double이 기본 부동 타입이며 정밀도를 위해 float보다 선호.

cpp
#include <cstdint>
int32_t a = 100;
int64_t big = 9'000'000'000LL;
uint8_t byte = 255;
size_t sz = sizeof(a);   // 4

float f = 1.5f;          // 4 bytes
double d = 3.14159265;   // 8 bytes (preferred)

숫자 제한

<limits>는 숫자 속성에 대한 타입 트레이트를 제공. 하드코딩된 INT_MAX 매크로 대신 이것을 사용. epsilon()은 부동 소수점으로 구별 가능한 가장 작은 차이를 주어 허용 오차로 double을 비교하는 데 유용.

cpp
#include <limits>
std::cout << std::numeric_limits<int>::max();    // 2147483647
std::cout << std::numeric_limits<int>::min();    // -2147483648
std::cout << std::numeric_limits<double>::infinity();
std::cout << std::numeric_limits<double>::epsilon();

수학 함수

<cmath>는 표준 수학 함수를 제공. 정수 오버플로우는 C++에서 정의되지 않은 동작; int64_t를 사용하거나 경계 확인. 재무 코드의 경우 부동 소수점이 부정확함을 기억 — 정수 센트나 십진수 라이브러리를 고려.

cpp
#include <cmath>
double x = 2.5;
std::pow(x, 3);     // 15.625
std::sqrt(x);       // 1.581
std::abs(-5);       // 5
std::floor(3.7);    // 3.0
std::ceil(3.2);     // 4.0
std::round(3.5);    // 4.0
std::fmod(10.5, 3); // 1.5

난수

현대 C++은 rand() 대신 <random> 라이브러리를 사용. mt19937은 고품질 PRNG. 분포(uniform_int, uniform_real, normal)는 rand() % N을 괴롭히는 모듈로 편향 없이 원시 비트를 원하는 범위로 매핑.

cpp
#include <random>
std::random_device rd;
std::mt19937 gen(rd());  // Mersenne Twister engine
std::uniform_int_distribution<int> dist(1, 100);

for (int i = 0; i < 5; i++) {
    std::cout << dist(gen) << " ";
}

정수 오버플로우 & 캐스팅

부호 있는 정수 오버플로우는 C++에서 정의되지 않은 동작(컴파일러가 발생하지 않는다고 가정하고 최적화 가능). 곱하기 전에 항상 더 넓은 타입으로 캐스팅하거나 경계 확인. 부호 없는 오버플로우는 2^n modulo로 래핑되며 잘 정의됨.

cpp
int a = 100'000;
int b = a * a;   // overflow! undefined behavior
long long safe = (long long)a * a;  // OK

// Check before multiplying
if (a > 0 && b > INT_MAX / a) {
    // would overflow, handle it
}
04

제어 흐름

If / Else

C++17은 초기화자가 있는 if를 도입: if (auto it = m.find(k); it != m.end()) { ... }. 이는 변수를 if/else 블록으로 범위 지정하여 주변 범위를 깔끔하게 유지하고 우발적 재사용을 방지.

cpp
int score = 85;
if (score >= 90) {
    std::cout << "A\n";
} else if (score >= 80) {
    std::cout << "B\n";
} else {
    std::cout << "C\n";
}

Switch

의도하지 않은 fall-through를 방지하기 위해 항상 break 포함. C++17 [[fallthrough]] 속성은 경고를 억제하기 위해 의도적인 fall-through를 문서화. switch는 문자열이나 실수가 아닌 정수와 열거형 타입에서 작동.

cpp
int day = 3;
switch (day) {
    case 1: std::cout << "Mon"; break;
    case 2: std::cout << "Tue"; break;
    case 3: std::cout << "Wed"; break;
    default: std::cout << "Other";
}

For 루프

범위 기반 for는 컨테이너를 깔끔하게 반복. 요소 복사를 피하기 위해 const auto& 사용(문자열과 큰 객체에 중요). 제자리에서 요소를 수정하려면 auto&(비 const 참조) 사용.

cpp
// Classic for
for (int i = 0; i < 5; i++) {
    std::cout << i << " ";
}

// Range-based for (C++11)
std::vector<int> v = {1, 2, 3};
for (int n : v) std::cout << n;
for (const auto &n : v) std::cout << n;  // no copy

While & Do-While

while은 실행 전에 조건 확인; do-while은 확인 전에 본문을 최소 한 번 실행. do-while은 조건을 평가하기 전에 본문이 실행되어야 하는 입력 검증과 메뉴 루프에 유용.

cpp
int n = 5;
while (n > 0) {
    std::cout << n-- << " ";
}

int x;
do {
    std::cin >> x;
} while (x < 0);  // runs at least once

Break, Continue & 중첩 루프

break는 가장 가까운 둘러싸는 루프를 종료; continue는 다음 반복으로 건너뜀. C++에는 Java 같은 레이블이 있는 break가 없음; 플래그 변수를 사용하거나 루프를 함수로 추출하고 return으로 중첩 루프에서 벗어나세요.

cpp
for (int i = 0; i < 10; i++) {
    if (i == 3) continue;  // skip 3
    if (i == 7) break;     // stop at 7
    std::cout << i << " ";  // 0 1 2 4 5 6
}

// C++ has no labeled break; use a flag
bool found = false;
for (int i = 0; i < n && !found; i++) {
    for (int j = 0; j < m; j++) {
        if (matrix[i][j] == target) { found = true; break; }
    }
}
05

함수 & 람다

정의 & 다중 반환

C++17 구조화 바인딩(auto [a, b] = ...)은 튜플, 쌍, 구조체를 깔끔하게 언팩. C++17 이전에는 std::tie나 출력 매개변수 사용. 값으로 반환은 복사를 생략하는 이동 의미론(RVO)으로 인해 저렴.

cpp
#include <tuple>
std::tuple<int, int> divide(int a, int b) {
    return {a / b, a % b};
}

auto [q, r] = divide(17, 5);  // q=3, r=2 (C++17)

기본값 & Inline

기본 인수는 호출자가 후행 매개변수를 생략할 수 있게. inline은 컴파일러에게 함수를 인라인으로 확장하라는 힌트; 현대 컴파일러는 최적화 플래그에 따라 스스로 결정하므로 inline은 주로 ODR(단일 정의 규칙)에 관한 것.

cpp
inline int power(int base, int exp = 2) {
    int r = 1;
    for (int i = 0; i < exp; i++) r *= base;
    return r;
}
// power(3) == 9, power(2, 5) == 32

함수 오버로딩

오버로딩은 함수가 이름을 공유하지만 매개변수 타입이 다르게 허용. 컴파일러는 오버로드 해결로 최적 일치 선택. 모호한 오버로드는 컴파일 에러 유발; 본문이 타입 간에 동일할 때 템플릿을 선호.

cpp
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
std::string add(std::string a, std::string b) { return a + b; }

add(1, 2);          // int version
add(1.5, 2.5);      // double version
add("a", "b");      // string version

람다 표현식

람다는 인라인으로 익명 함수 객체를 생성. []는 변수 캡처: [=] 값으로, [&] 참조로, [x] 특정 값을, [&x] 특정 참조로. STL 알고리즘과 콜백에 필수. 참조로 캡처할 때 댕글링 참조 주의.

cpp
auto square = [](int x) { return x * x; };
std::cout << square(5);  // 25

int factor = 3;
auto multiply = [factor](int x) { return x * factor; };

std::vector<int> v = {1, 2, 3};
std::for_each(v.begin(), v.end(), [](int n) {
    std::cout << n << " ";
});

함수 포인터 & std::function

std::function(<functional>에서)은 모든 callable을 보유: 함수, 람다, 펑터. 원시 함수 포인터보다 유연하지만 타입 소거로 인해 약간의 런타임 오버헤드. 콜백과 컨테이너에 callable을 저장하는 데 사용.

cpp
#include <functional>
int add(int a, int b) { return a + b; }

int (*fp)(int, int) = add;             // raw function pointer
std::function<int(int, int)> f = add;  // flexible wrapper
f = [](int a, int b) { return a * b; };

std::cout << f(3, 4);  // 12
06

STL 컨테이너

vector

vector는 동적 배열이자 기본 컨테이너 선택. push_back은 분할 상환 O(1). .at()은 경계 검사(std::out_of_range throw), operator[]는 안 함. 재할당을 피하기 위해 크기를 알면 선제적으로 reserve() 호출.

cpp
#include <vector>
std::vector<int> v = {1, 2, 3};
v.push_back(4);
v.pop_back();
v.size();          // 3
v[0] = 0;
v.at(5);           // throws std::out_of_range
for (int n : v) std::cout << n;

map & unordered_map

map은 키를 정렬된 상태로 유지(O(log n) 연산); unordered_map은 해싱 사용(O(1) 평균). 정렬된 반복이나 범위 쿼리가 필요하면 map; 순수 조회 속도에는 unordered_map. unordered_map 반복은 순서가 없음.

cpp
#include <map>
#include <unordered_map>
std::map<std::string, int> ordered;   // sorted by key (red-black tree)
ordered["Alice"] = 30;

std::unordered_map<std::string, int> hashed;  // hash table
hashed["Bob"] = 25;

for (auto &[k, v] : ordered) {  // C++17 structured binding
    std::cout << k << ":" << v;
}

set & unordered_set

set은 고유한 정렬된 요소 저장(O(log n)). unordered_set은 해시 기반 버전(O(1) 평균). 중복 제거와 멤버십 테스트에 사용. lower_bound/upper_bound는 정렬된 집합에서 범위 쿼리를 가능하게.

cpp
#include <set>
std::set<int> s = {3, 1, 4, 1, 5};
// s contains: 1, 3, 4, 5 (sorted, unique)
s.insert(2);
s.erase(1);
if (s.count(4)) std::cout << "found";
auto it = s.lower_bound(3);  // first >= 3

array & deque

array는 STL 인터페이스가 있는 고정 크기 스택 할당 배열(C 배열보다 안전, 포인터로 붕괴 없음). deque(양끝 큐)은 양쪽 끝에서 O(1) push/pop 지원, 앞이 O(n)인 vector와 달리.

cpp
#include <array>
#include <deque>
std::array<int, 3> arr = {1, 2, 3};  // fixed size, stack-allocated
arr.size();  // 3

std::deque<int> dq = {1, 2, 3};
dq.push_front(0);
dq.push_back(4);
// dq: 0, 1, 2, 3, 4

tuple & pair

tuple은 모든 타입의 이질적 값을 보유. pair는 2요소 tuple. 구조화 바인딩(C++17)은 이를 명명된 변수로 분해. 요소가 (키, 값)의 쌍인 map을 반복할 때 일반적.

cpp
#include <tuple>
std::tuple<int, std::string, double> t = {1, "Alice", 3.14};
auto [id, name, val] = t;  // C++17 structured binding

std::pair<int, int> p = {1, 2};
std::cout << p.first << p.second;

auto [a, b] = std::make_pair(10, 20);
07

포인터 & 메모리

원시 포인터

포인터는 메모리 주소를 저장. &는 주소를 가져오고, *는 역참조. 포인터 산술은 배열에서 작동. 원시 포인터는 소유권을 추적하지 않아 누수와 댕글링 포인터 유발 — 소유 리소스에는 스마트 포인터를 선호.

cpp
int x = 10;
int *ptr = &x;        // address of x
std::cout << *ptr;    // 10 (dereference)
*ptr = 20;
std::cout << x;       // 20

int arr[] = {1, 2, 3};
int *p = arr;
std::cout << *(p + 1);  // 2

참조 vs 포인터

참조는 더 안전(절대 null, 항상 유효)하고 더 깔끔한 구문. 함수 매개변수와 반환 값에는 참조 사용. null이 의미 있는 상태이거나 가리키는 것을 재할당해야 할 때 포인터 사용.

cpp
int x = 10;
int &ref = x;    // must init, cannot reseat
int *ptr = &x;   // can be null, can reassign

ref = 20;        // x = 20
*ptr = 30;       // x = 30

// References cannot be null, safer for parameters
void foo(const std::string &s);  // preferred
void bar(std::string *s);        // s might be null

unique_ptr

unique_ptr은 힙 객체의 단일 소유권. 복사 불가, 이동만 가능. 범위를 벗어나면 자동 삭제(RAII). 대부분의 사용 사례에 대한 기본 스마트 포인터 — 원시 포인터에 대한 제로 오버헤드.

cpp
#include <memory>
auto p = std::make_unique<int>(42);
std::cout << *p;  // 42
// auto p2 = p;   // ERROR: cannot copy
auto p2 = std::move(p);  // transfer ownership
// p is now nullptr

shared_ptr & weak_ptr

shared_ptr은 참조 카운팅 사용; 마지막 shared_ptr이 파괴되면 객체 해제. weak_ptr은 카운트에 영향 없이 관찰, 참조 순환을 끊음. shared_ptr의 순환 피하기(카운트가 0에 도달하지 않아 누수).

cpp
#include <memory>
auto a = std::make_shared<int>(42);
auto b = a;  // both point to same object
std::cout << a.use_count();  // 2

std::weak_ptr<int> w = a;  // observer, no ownership
if (auto locked = w.lock()) {
    std::cout << *locked;  // 42
}

RAII & new/delete

RAII는 리소스 수명을 객체 범위에 연결: 생성자는 획득, 소멸자는 해제. 예외가 전파될 때도 정리를 보장. 수동 new/delete보다 vector와 스마트 포인터를 선호 — 이들이 RAII를 구현.

cpp
// Manual new/delete (avoid in modern C++)
int *p = new int(42);
delete p;

// RAII: resource acquisition is initialization
class Buffer {
    int *data;
public:
    Buffer(size_t n) : data(new int[n]) {}
    ~Buffer() { delete[] data; }  // auto cleanup
};

// Modern: use containers/smart pointers instead
std::vector<int> buf(100);  // no manual delete needed
08

클래스 & OOP

클래스 & 생성자

멤버 초기화 목록(: name(...), age(...))은 본문 실행 전에 멤버를 초기화, 본문에서 할당하는 것보다 효율적. getter를 const로 표시하여 const 객체에서 호출 가능. std::move는 문자열 매개변수 복사를 피함.

cpp
class Person {
    std::string name;
    int age;
public:
    Person(std::string n, int a) : name(std::move(n)), age(a) {}
    std::string getName() const { return name; }
    int getAge() const { return age; }
};

Person p("Alice", 30);
std::cout << p.getName();

접근 한정자 & 캡슐화

private 멤버는 클래스 내에서만 접근 가능; protected는 서브클래스 허용; public은 모두에게 개방. 캡슐화는 구현 세부 정보를 숨기고 안정적인 인터페이스 노출. 기본적으로 private를 사용하고 필요한 것만 노출.

cpp
class Account {
private:
    double balance;
protected:
    std::string owner;
public:
    Account(double b) : balance(b) {}
    double getBalance() const { return balance; }
    void deposit(double amt) { if (amt > 0) balance += amt; }
};

상속 & Virtual

virtual은 런타임 다형성을 가능하게 — Animal*를 통해 speak() 호출은 Dog의 버전으로 디스패치. 기본 클래스에는 항상 가상 소멸자를 선언하여 기본 포인터로 삭제할 때 파생 소멸자 호출. override는 오타를 잡음.

cpp
class Animal {
public:
    virtual void speak() { std::cout << "..."; }
    virtual ~Animal() = default;  // virtual destructor!
};

class Dog : public Animal {
public:
    void speak() override { std::cout << "Woof"; }
};

Animal *a = new Dog();
a->speak();  // Woof (polymorphism)
delete a;

추상 클래스 & 인터페이스

순수 가상 함수(= 0)는 클래스를 추상적으로 만듦 — 인스턴스화 불가. 순수 가상만 있는 클래스는 Java 인터페이스처럼 작동. 구체적인 서브클래스는 모든 순수 가상을 구현하거나 추상적으로 유지.

cpp
class Shape {
public:
    virtual double area() const = 0;  // pure virtual
    virtual ~Shape() = default;
};

class Circle : public Shape {
    double r;
public:
    Circle(double r) : r(r) {}
    double area() const override {
        return 3.14159 * r * r;
    }
};

연산자 오버로딩

연산자 오버로딩은 사용자 타입이 +, <<, == 등과 작동하게 함. 의미가 직관적일 때만 오버로드(수학 타입, 반복자). << 연산자는 friend를 통해 스트림 출력을 위해 일반적으로 오버로드되어 cout << myObject 가능.

cpp
class Vec2 {
    double x, y;
public:
    Vec2(double x, double y) : x(x), y(y) {}
    Vec2 operator+(const Vec2 &o) const {
        return {x + o.x, y + o.y};
    }
    friend std::ostream &operator<<(std::ostream &os, const Vec2 &v) {
        return os << "(" << v.x << "," << v.y << ")";
    }
};

Vec2 a(1, 2), b(3, 4);
std::cout << a + b;  // (4,6)
09

템플릿 & 제네릭

함수 템플릿

함수 템플릿은 컴파일 타임에 타입별 버전을 생성. 컴파일러는 인수에서 T를 추론; 명시적으로 지정할 수도 있음. 템플릿은 제로 비용 추상화 — 런타임 오버헤드 없음, 하지만 컴파일 시간과 바이너리 크기 증가.

cpp
template <typename T>
T max_val(T a, T b) {
    return (a > b) ? a : b;
}

std::cout << max_val(3, 7);           // int: 7
std::cout << max_val(3.14, 2.71);     // double: 3.14
std::cout << max_val<std::string>("a", "b");  // explicit

클래스 템플릿

클래스 템플릿은 전체 클래스를 타입에 대해 매개변수화. 표준 컨테이너(vector, map)는 모두 템플릿. 컴파일러가 코드를 생성하기 위해 전체 정의가 필요하므로 템플릿 코드는 헤더에 있어야 함(또는 명시적 인스턴스화 사용).

cpp
template <typename T>
class Stack {
    std::vector<T> data;
public:
    void push(T v) { data.push_back(v); }
    T pop() { T v = data.back(); data.pop_back(); return v; }
    bool empty() const { return data.empty(); }
};

Stack<int> si;
si.push(1); si.push(2);
Stack<std::string> ss;
ss.push("hi");

템플릿 특수화

전체 특수화는 특정 타입에 대한 사용자 정의 구현 제공. 부분 특수화(클래스 템플릿만)는 타입 범주(예: 모든 포인터 타입)에 대해 사용자 정의. 최적화나 특수 케이스 동작에 유용.

cpp
template <typename T>
T identity(T x) { return x; }

// Full specialization for bool
template <>
bool identity<bool>(bool x) {
    std::cout << "bool!";
    return x;
}

identity(42);     // generic
identity(true);   // specialized

가변 템플릿

가변 템플릿은 매개변수 팩(...)을 통해 임의 개수의 인수를 받음. 각 인수를 처리하기 위해 재귀. C++17 폴드 표현식은 이를 단순화: (std::cout << ... << args). std::make_shared, std::tuple에서 많이 사용.

cpp
template <typename T>
void print(T v) { std::cout << v << "\n"; }

template <typename T, typename... Args>
void print(T first, Args... rest) {
    std::cout << first << ", ";
    print(rest...);  // recurse
}

print(1, "hello", 3.14);  // 1, hello, 3.14

Concepts (C++20)

Concepts(C++20)는 읽기 쉬운 요구 사항으로 템플릿 매개변수를 제한, 난해한 enable_if/SFINAE를 대체. 제약이 충족되지 않을 때 훨씬 더 명확한 에러 메시지 생성. std::integral, std::floating_point, std::convertible_to 같은 표준 concepts 사용.

cpp
#include <concepts>
template <std::integral T>
T add(T a, T b) { return a + b; }

template <typename T>
requires std::floating_point<T>
T half(T x) { return x / 2; }

// Abbreviated form
auto square(std::integral auto x) { return x * x; }
10

STL 알고리즘

sort & find

STL 알고리즘은 반복자 범위 [begin, end)에서 작동. sort는 O(n log n). find는 선형; 정렬된 범위에는 binary_search/lower_bound(O(log n)) 사용. 사용자 정의 순서를 위해 사용자 정의 비교기(람다나 std::greater) 전달.

cpp
#include <algorithm>
std::vector<int> v = {3, 1, 4, 1, 5, 9};
std::sort(v.begin(), v.end());          // 1 1 3 4 5 9
std::sort(v.begin(), v.end(), std::greater<>());  // descending

auto it = std::find(v.begin(), v.end(), 4);
if (it != v.end()) std::cout << "found";

transform & for_each

transform은 각 요소를 새 값으로 매핑(함수형 언어의 map처럼). for_each는 부작용을 위해 함수 적용. C++20 ranges는 begin/end 반복자 없이 더 깔끔한 파이프라인 스타일을 위해 v | views::transform(...)을 허용.

cpp
std::vector<int> v = {1, 2, 3, 4};
std::vector<int> squared(v.size());
std::transform(v.begin(), v.end(), squared.begin(),
               [](int x) { return x * x; });
// squared: 1 4 9 16

std::for_each(v.begin(), v.end(), [](int &x) { x *= 2; });
// v: 2 4 6 8

accumulate & count

accumulate(<numeric>에서)는 연산으로 범위를 접음. 세 번째 인수는 초기값이며 결과 타입을 결정 — double 합에는 0.0 사용. count는 값과 같은 요소 수 반환; count_if는 술어 사용.

cpp
#include <numeric>
std::vector<int> v = {1, 2, 3, 4, 5};
int sum = std::accumulate(v.begin(), v.end(), 0);  // 15
int product = std::accumulate(v.begin(), v.end(), 1,
                              std::multiplies<>());  // 120

int cnt = std::count(v.begin(), v.end(), 3);  // 1

copy, remove & unique

remove는 실제로 지우지 않음 — 일치하지 않는 요소를 앞으로 이동하고 새 끝 반복자 반환. erase-remove 관용구를 위해 .erase()와 짝지음. unique도 마찬가지로 연속 중복을 압축; 완전히 중복 제거하려면 먼저 정렬.

cpp
std::vector<int> v = {1, 2, 3, 4, 5};
std::vector<int> dest;
std::copy(v.begin(), v.end(), std::back_inserter(dest));

auto end = std::remove(v.begin(), v.end(), 3);  // erase-remove idiom
v.erase(end, v.end());

std::vector<int> u = {1, 1, 2, 2, 3};
u.erase(std::unique(u.begin(), u.end()), u.end());  // 1 2 3

min, max & clamp

min/max는 두 값이나 초기화 목록 중 더 작은/큰 값 반환. minmax는 둘 다 쌍으로 반환. clamp(C++17)는 값을 범위로 제한, 수동 if/else 경계 검사 대체 — 입력 검증과 UI 좌표에 유용.

cpp
int a = 3, b = 7;
std::cout << std::max(a, b);   // 7
std::cout << std::min(a, b);   // 3

auto [mn, mx] = std::minmax({3, 1, 4, 1, 5});  // mn=1, mx=5

int score = 105;
int clamped = std::clamp(score, 0, 100);  // 100
11

에러 처리

예외: try/catch

값으로 예외를 throw, 슬라이싱을 피하기 위해 const 참조로 catch. std::exception을 catch하면 기본 클래스를 통해 모든 표준 예외 catch. 예외는 정상적인 제어 흐름이 아닌 예외적인 경우용 — throw 시 오버헤드.

cpp
#include <stdexcept>
try {
    int x = 10, y = 0;
    if (y == 0) throw std::runtime_error("division by zero");
    std::cout << x / y;
} catch (const std::exception &e) {
    std::cerr << "Error: " << e.what();
}

표준 예외 계층

<stdexcept>는 표준 예외 타입 제공. logic_error는 프로그래머 에러(런타임 전에 감지 가능); runtime_error는 예상치 못한 런타임 조건. 사용자 정의 예외는 std::runtime_error에서 파생하여 표준 catch 블록과 통합.

cpp
#include <stdexcept>
throw std::runtime_error("runtime error");
throw std::logic_error("logic error");
throw std::out_of_range("index out of range");
throw std::invalid_argument("bad argument");
throw std::bad_alloc();  // thrown by new on allocation failure

// std::exception is the base; .what() returns the message

사용자 정의 예외

사용자 정의 예외를 표준 기반에서 파생하여 catch(const std::exception&)과 통합. 디버깅에 도움이 되는 컨텍스트 필드(파일 경로, 에러 코드) 추가. what()이 작동하도록 항상 메시지를 기본 생성자에 전달.

cpp
class FileError : public std::runtime_error {
public:
    FileError(const std::string &msg, const std::string &path)
        : std::runtime_error(msg + ": " + path), path_(path) {}
    const std::string &path() const { return path_; }
private:
    std::string path_;
};

try { throw FileError("not found", "data.txt"); }
catch (const FileError &e) {
    std::cerr << e.what() << " at " << e.path();
}

noexcept & RAII 안전

noexcept는 함수가 throw하지 않음을 약속, 컴파일러 최적화 가능. throw하면 std::terminate 호출. RAII는 스택 해제 중에 소멸자 실행을 보장하여 예외가 호출 스택을 통해 전파될 때도 리소스 해제.

cpp
void safe_op() noexcept {
    // guaranteed not to throw
}

void risky() {
    std::vector<int> v(1000);  // RAII: cleanup on exception
    throw std::runtime_error("oops");
    // v's destructor still runs during stack unwinding
}

어설션

assert()는 디버그 빌드에서 조건 확인; 릴리스(NDEBUG 정의 시)에서 제거되므로 프로덕션 검사에 사용하지 마세요. 버그를 나타내는 내부 불변성에 사용. 사용자 대면 검증에는 예외를 throw하거나 에러 코드 반환.

cpp
#include <cassert>
double sqrt_safe(double x) {
    assert(x >= 0 && "sqrt of negative");
    return std::sqrt(x);
}
// In release builds (NDEBUG defined), assert is removed
12

파일 I/O & 스트림

파일 읽기

ifstream은 파일을 읽기 위해 엶. 열기 성공 여부를 항상 확인(실패 시 !file이 true). getline은 공백을 포함하여 줄 단위로 읽음. 스트림의 소멸자가 파일을 자동으로 닫음(RAII), 수동 close 불필요.

cpp
#include <fstream>
#include <string>
std::ifstream file("input.txt");
if (!file) { std::cerr << "cannot open"; return 1; }

std::string line;
while (std::getline(file, line)) {
    std::cout << line << "\n";
}

파일 쓰기

ofstream은 파일에 쓰기, 기본적으로 잘라냄. 추가에는 std::ios::app, 바이너리 모드에는 std::ios::binary 사용. << 연산자는 std::cout과 정확히 같이 작동. out.flush()로 플러시하거나 std::endl(이것도 플러시) 사용.

cpp
#include <fstream>
std::ofstream out("output.txt");
if (!out) return 1;

out << "Line 1\n";
out << "Value: " << 42 << "\n";

out.close();  // optional, destructor closes
// Append mode: std::ofstream("f.txt", std::ios::app);

문자열 스트림

ostringstream은 혼합 타입에서 문자열 빌드(버퍼처럼). istringstream은 문자열을 타입이 지정된 값으로 파싱. 직접 문자열 작업보다 느리지만 직렬화, URL 빌드, 토큰 파싱에 매우 편리.

cpp
#include <sstream>
// Build a string from mixed types
std::ostringstream oss;
oss << "Name=" << "Alice" << "&Age=" << 30;
std::string url = oss.str();

// Parse tokens from a string
std::istringstream iss("10 20 30");
int a, b, c;
iss >> a >> b >> c;

바이너리 파일

바이너리 모드는 줄바꿈 변환을 피하고 텍스트보다 간결. write/read는 char*와 바이트 수를 받음 — 구조체에는 reinterpret_cast 사용. 주의: 바이너리 파일은 아키텍처 간 이식성 없음(엔디안, 구조체 패딩 다름).

cpp
#include <fstream>
struct Record { int id; double value; };

// Write
std::ofstream out("data.bin", std::ios::binary);
Record r{1, 3.14};
out.write(reinterpret_cast<char*>(&r), sizeof(r));

// Read
std::ifstream in("data.bin", std::ios::binary);
Record r2;
in.read(reinterpret_cast<char*>(&r2), sizeof(r2));

형식화된 출력 (C++20 fmt)

std::format(C++20)은 Python 스타일 형식 문자열을 C++로 가져와, 지저분한 iomanip 조작자를 대체. 오래된 코드의 경우 <iomanip>가 setprecision, setw, setfill 제공. {fmt} 라이브러리는 같은 구문의 인기 있는 C++20 이전 대안.

cpp
#include <format>  // C++20
std::string s = std::format("Hello, {}! You are {}.", "Alice", 30);
std::cout << std::format("{:.2f}", 3.14159);  // 3.14
std::cout << std::format("{:>10}", "right");   // padded

// Pre-C++20: use iomanip
#include <iomanip>
std::cout << std::fixed << std::setprecision(2) << 3.14159;
13

스마트 포인터

unique_ptr - 단일 소유권

unique_ptr은 기본 스마트 포인터 — 소유자 하나면 충분할 때 사용. 원시 포인터 대비 제로 오버헤드. make_unique 선호(예외 안전). 복사 불가, 이동만 가능. 사용자 정의 삭제자는 FILE*나 소켓 같은 C 리소스에 RAII를 가능하게.

cpp
#include <memory>
std::unique_ptr<int> p1 = std::make_unique<int>(42);
// std::unique_ptr<int> p2 = p1;  // ERROR: cannot copy
std::unique_ptr<int> p2 = std::move(p1);  // OK: transfer ownership
// p1 is now nullptr

// Custom deleter
auto deleter = [](FILE* f) { if (f) fclose(f); };
std::unique_ptr<FILE, decltype(deleter)> fp(fopen("f.txt", "r"), deleter);

shared_ptr - 공유 소유권

shared_ptr은 참조 카운팅 사용 — 여러 포인터가 같은 객체를 소유 가능. 참조 카운트가 0이 되면 객체 파괴. make_shared 선호(객체 + 제어 블록에 단일 할당). 원자적 참조 카운트와 제어 블록으로 인해 unique_ptr보다 무거움. 소유권이 진정으로 공유될 때 사용.

cpp
#include <memory>
auto p1 = std::make_shared<int>(42);
auto p2 = p1;  // OK: both share ownership
std::cout << *p1 << " " << p1.use_count();  // 42 2

// Control block holds refcount + deleter + allocator
// Refcount is atomic (thread-safe), but object access is NOT
std::shared_ptr<int> p3{new int{10}};  // uses non-array new

// Thread-safe: refcount operations are atomic
// NOT thread-safe: accessing the pointed-to object

weak_ptr - 순환 끊기

weak_ptr은 shared_ptr의 비소유 관찰자. 참조 카운트 증가 안 함. lock()으로 임시 shared_ptr 획득(객체가 파괴된 경우 null 반환). 메모리 누수를 유발할 참조 순환(예: 이중 연결 리스트, 부모-자식 관계)을 끊는 데 필수.

cpp
#include <memory>
struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;  // weak to avoid cycle
};

auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->prev = a;  // weak_ptr from shared_ptr

// To use a weak_ptr, lock() into a shared_ptr
if (auto locked = b->prev.lock()) {
    std::cout << "prev exists";
} else {
    std::cout << "prev expired";
}

배열이 있는 스마트 포인터

스마트 포인터는 배열을 관리 가능. unique_ptr<T[]>는 operator[]와 올바른 delete[] 제공. shared_ptr<T[]>는 C++17부터 지원. 하지만 std::vector나 std::array가 거의 항상 더 나음 — 더 안전하고, 더 인체공학적이며, 자동 문서화. 레거시 API와 인터페이스할 때만 스마트 배열 포인터 사용.

cpp
#include <memory>
// C++17: shared_ptr supports arrays
std::shared_ptr<int[]> arr1(new int[10]);
arr1[0] = 42;

// unique_ptr with arrays (partial specialization exists)
std::unique_ptr<int[]> arr2 = std::make_unique<int[]>(10);
arr2[5] = 100;

// Prefer std::array or std::vector over raw arrays
#include <vector>
std::vector<int> v(10);  // better choice

enable_shared_from_this

객체가 자신에 대한 shared_ptr을 반환해야 할 때, enable_shared_from_this는 안전한 shared_from_this()를 제공합니다. shared_ptr<T>(this)를 직접 호출하면 두 번째 제어 블록이 생성되어 double-free로 이어집니다. 이 객체는 이미 shared_ptr에 의해 관리되고 있어야 하며, 그렇지 않으면 shared_from_this()가 bad_weak_ptr을 던집니다.

cpp
#include <memory>
class Widget : public std::enable_shared_from_this<Widget> {
public:
    std::shared_ptr<Widget> getPtr() {
        return shared_from_this();  // safe
        // return shared_ptr<Widget>(this);  // BAD: double delete
    }
};

auto w = std::make_shared<Widget>();
auto w2 = w->getPtr();  // shares ownership, refcount = 2
14

이동 의미론과 Rvalue 참조

Lvalues, Rvalues, 그리고 참조

Lvalue는 정체성을 가지며 단일 표현식을 넘어 지속됩니다(이름 있는 객체). Rvalue는 임시 또는 리터럴 값입니다. T&는 lvalue에 바인딩되고, T&&는 rvalue에 바인딩됩니다. const T&는 특별하여 둘 모두에 바인딩됩니다. 이 구분을 이해하는 것이 이동 의미론의 기초입니다.

cpp
int x = 10;          // x is an lvalue
int& lref = x;       // lvalue reference
int&& rref = 20;     // rvalue reference (binds to temporary)
// int& bad = 20;    // ERROR: can't bind lref to rvalue
const int& cref = 20;  // OK: const lref binds to rvalue

int y = x + 5;       // (x + 5) is an rvalue (prvalue)
std::string s1 = "hi";
std::string&& rr = std::move(s1);  // rr is named rvalue

std::move와 이동 생성자

std::move는 아무것도 이동시키지 않습니다—rvalue로 캐스팅하여 이동 생성자/대입 연산자가 선택되도록 합니다. 이동 연산은 noexcept여야 컨테이너가 재할당 중에 사용할 수 있습니다(그렇지 않으면 예외 안전성을 위해 복사로 폴백합니다). 이동 후 원본 객체는 유효하지만 지정되지 않은 상태가 됩니다.

cpp
class Buffer {
    int* data;
    size_t size;
public:
    // Move constructor: steal resources
    Buffer(Buffer&& other) noexcept
        : data(other.data), size(other.size) {
        other.data = nullptr;
        other.size = 0;
    }
    // Move assignment
    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data; size = other.size;
            other.data = nullptr; other.size = 0;
        }
        return *this;
    }
};

Buffer a(100);
Buffer b = std::move(a);  // calls move ctor, a is now empty

완벽한 전달

완벽한 전달은 인수의 값 카테고리(lvalue vs rvalue)를 보존하면서 다른 함수로 전달합니다. 연역된 문맥에서 T&&는 '전달 참조'(forwarding reference, rvalue 참조가 아님)입니다. std::forward<T>는 조건부로 캐스팅합니다: T가 T&이면 lvalue, T가 T&&이면 rvalue입니다. 팩토리 함수와 래퍼에 필수적입니다.

cpp
#include <utility>
template <typename T, typename Arg>
auto make_unique(Arg&& arg) {
    return std::unique_ptr<T>(new T(std::forward<Arg>(arg)));
}

template <typename... Args>
void log(Args&&... args) {
    // std::forward preserves value category
    log_impl(std::forward<Args>(args)...);
}

std::string s = "hello";
log(s);              // Arg = std::string&  (lvalue)
log(std::string());  // Arg = std::string&& (rvalue)

5의 법칙 / 0의 법칙

5의 법칙: 클래스가 자원을 관리한다면 소멸자, 복사 생성자, 복사 대입, 이동 생성자, 이동 대입을 정의해야 합니다. 0의 법칙: RAII 타입(vector, string, 스마트 포인터)을 조합하여 컴파일러 생성 특수 멤버가 올바르게 만들어지도록 합니다. 이는 버그 발생이 쉬운 수동 자원 관리를 제거합니다.

cpp
// Rule of Five: if you define any of these, define all 5
class Resource {
    int* data;
public:
    Resource(size_t n) : data(new int[n]) {}
    ~Resource() { delete[] data; }
    Resource(const Resource& o);             // copy ctor
    Resource& operator=(const Resource& o);  // copy assign
    Resource(Resource&& o) noexcept;         // move ctor
    Resource& operator=(Resource&& o) noexcept;  // move assign
};

// Rule of Zero: prefer to use RAII types
class Better {
    std::vector<int> data;  // handles everything
public:
    Better() = default;  // compiler-generated funcs are correct
};

반환 값 최적화(RVO/NRVO)

RVO/NRVO는 컴파일러가 반환 값을 호출자의 저장 공간에 직접 생성하도록 하여 복사/이동을 완전히 피합니다. C++17은 prvalue에 대해 RVO를 필수로 만듭니다. return std::move(local)을 절대 작성하지 마세요—NRVO를 방해하고 (더 느린) 이동을 강제합니다. 지역 변수를 이름으로 반환하고 컴파일러가 최적화하도록 두세요.

cpp
std::vector<int> makeVec() {
    std::vector<int> v;
    v.push_back(1); v.push_back(2);
    return v;  // NRVO: no copy, no move!
}

std::string makeStr() {
    return std::string("hello");  // RVO
}

// C++17 guarantees RVO (mandatory copy elision) for prvalues
std::vector<int> v = makeVec();  // constructed in place

// Don't std::move return values—it disables RVO!
std::vector<int> bad() {
    std::vector<int> v;
    return std::move(v);  // WORSE: forces move, blocks NRVO
}
15

동시성(thread, mutex, async)

std::thread 기초

std::thread는 새 OS 스레드를 시작합니다. 스레드 객체가 소멸되기 전에 반드시 join()(대기) 또는 detach()(독립적으로 실행)를 호출해야 하며, 그렇지 않으면 std::terminate가 호출됩니다. 인수는 기본적으로 값으로 전달됩니다—참조에는 std::ref를, 이동 전용 타입에는 std::move를 사용하세요. 분리할 명확한 이유가 없다면 join을 선호하세요.

cpp
#include <thread>
#include <iostream>

void worker(int id) {
    std::cout << "Thread " << id << "\n";
}

int main() {
    std::thread t1(worker, 1);
    std::thread t2([]{ std::cout << "Lambda thread\n"; });

    t1.join();   // wait for t1 to finish
    t2.join();   // must join or detach before destruction
    // t1.detach();  // runs independently (risky)

    // Hardware concurrency hint
    unsigned n = std::thread::hardware_concurrency();
}

뮤텍스와 Lock Guard

공유 데이터는 항상 뮤텍스로 보호하세요. std::lock_guard는 가장 단순한 RAII 잠금입니다—생성 시 획득, 소멸 시 해제합니다. std::scoped_lock(C++17)은 교착 상태 회피 알고리즘으로 여러 뮤텍스를 안전하게 잠급니다. std::unique_lock은 조건 변수와 함께 사용하기 위해 더 많은 유연성(수동 lock/unlock, 지연 잠금)을 제공합니다.

cpp
#include <mutex>
#include <vector>

std::mutex mtx;
std::vector<int> shared;

void safePush(int val) {
    // RAII lock: unlocks even if exception thrown
    std::lock_guard<std::mutex> lock(mtx);
    shared.push_back(val);
}  // lock released here

// std::scoped_lock (C++17) locks multiple mutexes deadlock-free
std::mutex m1, m2;
void transfer() {
    std::scoped_lock lock(m1, m2);  // atomic
}

// std::unique_lock: lockable/unlockable, movable
std::unique_lock<std::mutex> ul(mtx);
ul.unlock();
ul.lock();

조건 변수

condition_variable은 스레드가 조건을 기다리게 합니다. 거짓 깨움을 처리하려면 wait()에 항상 술어를 사용하세요. wait()를 호출할 때 뮤텍스는 unique_lock이 보유해야 하며, 대기 중 해제되고 반환 전 재획득합니다. notify_one은 하나의 대기자를 깨우고, notify_all은 모두 깨웁니다. 이 패턴은 스레드 안전 큐와 생산자-소비자 파이프라인을 구현합니다.

cpp
#include <condition_variable>
#include <queue>
#include <thread>

std::mutex mtx;
std::condition_variable cv;
std::queue<int> tasks;

void producer() {
    {
        std::lock_guard<std::mutex> lock(mtx);
        tasks.push(42);
    }
    cv.notify_one();  // wake one waiting consumer
}

void consumer() {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, []{ return !tasks.empty(); });  // predicate prevents spurious wakeup
    int task = tasks.front();
    tasks.pop();
}

std::async와 Future

std::async는 작업을 비동기적으로 실행하는 고수준 방법이며, future를 반환합니다. std::launch::async는 새 스레드를 강제하고; std::launch::deferred는 get()에서 지연 실행합니다. 기본 정책은 둘 중 하나를 선택할 수 있으므로 예측 가능한 동작을 위해 명시하세요. 더 많은 제어를 원하면 std::promise/future 쌍을 사용하세요. future는 소멸 전에 반드시 get()을 호출하세요, 그렇지 않으면 소멸자가 차단될 수 있습니다.

cpp
#include <future>
#include <iostream>

int slowComputation() {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    return 42;
}

// async policy: may run in new thread or deferred
auto fut = std::async(std::launch::async, slowComputation);
// do other work...
int result = fut.get();  // blocks until ready

// std::launch::async: definitely new thread
// std::launch::deferred: lazy, runs on get()
// std::launch::async | deferred: implementation chooses

// std::promise for manual control
std::promise<int> p;
std::future<int> f = p.get_future();
std::thread([&p]{ p.set_value(10); }).detach();
f.get();  // 10

원자적 연산

std::atomic은 기본 타입에 대해 잠금 없는 스레드 안전 연산을 제공합니다. 일반 int보다 무겁지만 단순한 카운터/플래그에 대해 뮤텍스보다는 훨씬 가볍습니다. 메모리 순서는 가시성에 영향을 미칩니다: relaxed(순서 없음), acquire/release(동기화를 위한 쌍), seq_cst(기본값, 가장 강력함). 카운터/플래그에는 원자적 연산을; 복잡한 임계 구역에는 뮤텍스를 사용하세요.

cpp
#include <atomic>
#include <iostream>

std::atomic<int> counter{0};
std::atomic<bool> ready{false};

void worker() {
    while (!ready.load(std::memory_order_acquire));
    counter.fetch_add(1, std::memory_order_relaxed);
}

// Compare-and-swap (CAS) loop
std::atomic<int> val{0};
int expected = 0;
bool success = val.compare_exchange_weak(
    expected, 1,
    std::memory_order_acq_rel);

// Atomic is lock-free for most primitive types
static_assert(std::atomic<int>::is_always_lock_free);

// std::atomic_flag: guaranteed lock-free, for spinlocks
std::atomic_flag spin = ATOMIC_FLAG_INIT;
while (spin.test_and_set(std::memory_order_acquire));  // acquire
spin.clear(std::memory_order_release);  // release
16

템플릿 메타프로그래밍

템플릿 특수화

템플릿 특수화는 특정 타입에 대한 사용자 정의 구현을 제공합니다. 완전 특수화는 모든 템플릿 매개변수를 고정합니다. 부분 특수화(클래스 템플릿에만)는 일부 매개변수를 특수화하고 나머지는 제네릭으로 유지합니다. 타입 트레잇, std::vector<bool>, 알려진 타입에 대한 최적화에 많이 사용됩니다.

cpp
// Primary template
template <typename T>
struct TypeName {
    static const char* get() { return "unknown"; }
};

// Full specialization for int
template <>
struct TypeName<int> {
    static const char* get() { return "int"; }
};

// Full specialization for const char*
template <>
struct TypeName<const char*> {
    static const char* get() { return "string"; }
};

std::cout << TypeName<int>::get();           // "int"
std::cout << TypeName<double>::get();        // "unknown"
std::cout << TypeName<const char*>::get();   // "string"

SFINAE와 enable_if

SFINAE(Substitution Failure Is Not An Error)는 타입 속성에 따라 템플릿 오버로드를 활성화/비활성화합니다. std::enable_if는 조건부로 타입을 정의합니다. 대입이 실패하면 오버로드는 오류를 발생시키는 대신 조용히 제거됩니다. C++17의 if constexpr과 C++20 컨셉트가 종종 더 깔끔한 구문으로 SFINAE를 대체합니다.

cpp
#include <type_traits>

// SFINAE: Substitution Failure Is Not An Error
// Only enable this overload if T is integral
template <typename T,
          typename = std::enable_if_t<std::is_integral_v<T>>>
void process(T x) { std::cout << "integral\n"; }

// Only enable if T is floating point
template <typename T,
          std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
void process(T x) { std::cout << "floating\n"; }

process(10);    // integral
process(3.14);  // floating

// void_t trick (C++17) for detecting member existence
template <typename T, typename = void>
struct has_size : std::false_type {};
template <typename T>
struct has_size<T, std::void_t<decltype(std::declval<T>().size())>>
    : std::true_type {};

if constexpr(C++17)

if constexpr는 컴파일 타임에 조건을 평가하고 거짓인 분기를 완전히 버립니다(타입 검사조차 하지 않음). 이는 많은 SFINAE 패턴을 훨씬 깔끔한 코드로 대체합니다. 템플릿 재귀(기저 케이스가 재귀를 종료)와 인스턴스화 오류 없이 타입 트레잇에 따라 분기하는 데 특히 유용합니다.

cpp
#include <type_traits>
template <typename T>
auto getValue(T t) {
    if constexpr (std::is_pointer_v<T>) {
        return *t;  // only compiled if T is a pointer
    } else if constexpr (std::is_integral_v<T>) {
        return t * 2;
    } else {
        return t;
    }
}

int x = 5;
int* p = &x;
getValue(x);   // 10
getValue(p);   // 5
getValue(3.14);  // 3.14

// Compile-time recursion with termination
template <int N>
constexpr int factorial() {
    if constexpr (N <= 1) return 1;
    else return N * factorial<N - 1>();
}

가변 템플릿과 폴드 표현식

가변 템플릿은 매개변수 팩(typename... Args)을 통해 임의의 개수의 인수를 받습니다. C++17 폴드 표현식은 모든 팩 요소에 연산자를 적용합니다: 단항 폴드(... op pack), 이항 폴드(init op ... op pack). C++17 이전에는 기저 케이스가 있는 재귀가 필요했습니다. 가변 템플릿은 std::make_unique, std::tuple, printf 유사 함수의 기반입니다.

cpp
#include <iostream>

// Parameter pack
template <typename... Args>
void print(Args... args) {
    // C++17 fold expression
    ((std::cout << args << " "), ...);
    std::cout << "\n";
}

print(1, "hello", 3.14, 'x');  // 1 hello 3.14 x

// Sum with fold
template <typename... T>
auto sum(T... args) {
    return (args + ...);  // binary fold: ((a+b)+c)+d
}

// Sum with initial value
template <typename... T>
auto sumFrom0(T... args) {
    return (0 + ... + args);  // left fold with init
}

// Base case recursion (pre-C++17)
template <typename T>
void printOne(T t) { std::cout << t; }

컨셉트(C++20)

컨셉트(C++20)는 가독성이 좋고 의도를 드러내는 제약으로 SFINAE를 대체합니다. SFINAE보다 훨씬 나은 오류 메시지를 생성합니다. concept Name = constraint;로 컨셉트를 정의하세요. 템플릿 매개변수, requires 절, 또는 축약 템플릿(컨셉트와 함께 auto)에서 사용하세요. 표준 라이브러리는 <concepts>에 유용한 컨셉트를 많이 제공합니다.

cpp
#include <concepts>

// Define a concept
template <typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;

// Use as constraint
template <Numeric T>
T add(T a, T b) { return a + b; }

// Requires clause
template <typename T>
requires requires(T t) { t.size(); }
auto getSize(const T& t) { return t.size(); }

// Concepts in abbreviated function templates
void process(std::integral auto x) { /* ... */ }
void process(std::ranges::range auto& r) { /* ... */ }

// Standard concepts: integral, floating_point, equality_comparable,
// totally_ordered, copyable, movable, default_initializable, etc.
17

람다 표현식 심층 분석

캡처 모드

람다는 둘러싼 스코프에서 변수를 캡처합니다. [=]는 모두 값으로, [&]는 참조로 캡처합니다—편리하지만 오류가 발생하기 쉽습니다(매달린 참조, 의도치 않은 캡처). 명확성을 위해 명시적 캡처 [x, &y]를 선호하세요. 초기화 캡처 [name = expr](C++14)는 이름 변경, 이동, 캡처된 값의 계산을 허용합니다. [&]는 신중하게 캡처하세요—람다가 스코프보다 오래 살아남으면 매달린 참조가 발생합니다.

cpp
int x = 10, y = 20;
std::vector<int> v;

auto a = [] { /* no capture */ };
auto b = [x] { return x; };           // capture x by value
auto c = [&x] { x = 100; };           // capture x by reference
auto d = [=] { return x + y; };       // capture all by value
auto e = [&] { x = 1; y = 2; };       // capture all by reference
auto f = [x, &y] { return x + y; };   // mixed
auto g = [=, &x] { x = 1; return y; }; // default value, x by ref
auto h = [this] { return member; };   // capture this (C++17: *this)
auto i = [x = x + 5] { return x; };   // init capture (C++14)

// Best practice: capture only what you need, explicitly

제네릭 람다(C++14)

제네릭 람다는 auto 매개변수(C++14) 또는 명시적 템플릿 매개변수(C++20)를 사용합니다. 본질적으로 컴파일러 생성 템플릿 operator() 오버로드입니다. C++20 템플릿 람다는 타입 매개변수 T에 직접 접근할 수 있게 합니다. 재귀 람다는 std::function(또는 C++23의 deducing this)이 필요한데, 일반 auto 람다는 타입이 알려지기 전에 이름으로 자신을 참조할 수 없기 때문입니다.

cpp
// auto parameters (C++14)
auto add = [](auto a, auto b) { return a + b; };
add(1, 2);        // int
add(1.5, 2.5);    // double
add(std::string("a"), std::string("b"));

// Template lambdas (C++20)
auto getSize = []<typename T>(const std::vector<T>& v) {
    return v.size();
};

// Perfect forwarding in lambdas (C++20)
auto wrapper = []<typename T>(T&& arg) {
    return process(std::forward<T>(arg));
};

// Recursive lambda with std::function
#include <functional>
std::function<int(int)> fib = [&](int n) {
    return n < 2 ? n : fib(n - 1) + fib(n - 2);
};

mutable과 상태 저장 람다

기본적으로 람다에서 값으로 캡처된 변수는 const입니다—operator() 호출이 const입니다. mutable 키워드는 이 const 성질을 제거하여 값으로 캡처된 변수를 수정할 수 있게 합니다(원본이 아닌 복사본 수정). 이는 카운터 같은 상태 저장 람다를 가능하게 합니다. 람다의 각 복사본은 자체 캡처된 상태를 가집니다. std::function을 사용하여 다른 타입의 람다를 컨테이너에 저장하세요.

cpp
#include <functional>
int counter = 0;

// Without mutable, value-captured vars are const
auto inc = [counter]() mutable {
    return ++counter;  // modifies the captured copy
};
inc(); inc();
std::cout << counter;  // still 0 (original unchanged)

// Stateful counter with std::function
auto makeCounter(int start) {
    return [count = start]() mutable { return ++count; };
}
auto c = makeCounter(10);
c(); c();  // 11, 12

// Lambda as callback storage
std::vector<std::function<void()>> callbacks;
callbacks.push_back([&]{ std::cout << "click"; });

STL 알고리즘과 람다

람다는 STL 알고리즘의 술어/비교자로 빛을 발합니다. 기존의 함수자(함수 객체) 접근 방식을 인라인의 가독성 좋은 코드로 대체합니다. 일반적인 용도: sort의 비교자, find_if/remove_if/copy_if의 술어, transform의 변환. C++20 ranges는 파이프 연산자로 조합 가능한 변환을 통해 이를 더 깔끔하게 만듭니다.

cpp
#include <algorithm>
#include <vector>

std::vector<int> v = {5, 2, 8, 1, 9, 3};

// Sort descending
std::sort(v.begin(), v.end(), [](int a, int b) {
    return a > b;
});

// Find first even
auto it = std::find_if(v.begin(), v.end(),
    [](int x) { return x % 2 == 0; });

// Remove odds (erase-remove idiom)
v.erase(std::remove_if(v.begin(), v.end(),
    [](int x) { return x % 2 != 0; }), v.end());

// Transform
std::transform(v.begin(), v.end(), v.begin(),
    [](int x) { return x * x; });

// C++20 ranges: cleaner
auto evens = v | std::views::filter([](int x){ return x%2==0; })
               | std::views::transform([](int x){ return x*2; });

고차 함수

람다는 C++에서 함수형 프로그래밍 패턴을 가능하게 합니다. 함수는 람다(클로저)를 반환하거나 매개변수로 받을 수 있습니다. std::function은 타입 소거 저장을 위해 호출 가능한 모든 것을 감쌉니다. 함수 합성은 람다를 연결하여 파이프라인을 구축합니다. 이 스타일은 콜백, 이벤트 핸들러, 데이터 변환 파이프라인에 강력하지만, 템플릿 기반 접근은 std::function 오버헤드를 피합니다.

cpp
#include <functional>
// Function returning a function
auto makeMultiplier(int factor) {
    return [factor](int x) { return x * factor; };
}
auto doubleIt = makeMultiplier(2);
auto tripleIt = makeMultiplier(3);
doubleIt(5);  // 10
tripleIt(5);  // 15

// Function taking a function
template <typename F>
void applyTwice(int x, F f) {
    std::cout << f(f(x));
}
applyTwice(5, [](int x){ return x + 3; });  // 11

// Composition
auto compose = [](auto f, auto g) {
    return [f, g](auto x) { return f(g(x)); };
};
auto addOne = [](int x){ return x + 1; };
auto square = [](int x){ return x * x; };
auto sqThenAdd = compose(addOne, square);
sqThenAdd(3);  // 10
18

네임스페이스와 모듈

네임스페이스 기초

네임스페이스는 대형 프로젝트에서 이름 충돌을 방지합니다. 중첩과 별칭이 가능합니다. 익명 네임스페이스는 심볼에 내부 링크를 부여하여(현재 번역 단위에서만 보임), 이 목적을 위해 C 스타일의 static 키워드를 대체합니다. 헤더에서 using namespace를 피하세요—모든 포함 파일을 오염시킵니다.

cpp
namespace math {
    double pi = 3.14159;
    double square(double x) { return x * x; }

    namespace geometry {  // nested
        double circleArea(double r) { return pi * square(r); }
    }
}

// Usage
double a = math::square(5);
double area = math::geometry::circleArea(2.0);

// Namespace alias
namespace geo = math::geometry;
geo::circleArea(1.0);

// Anonymous namespace: internal linkage (like 'static')
namespace {
    int internalVar = 42;  // only visible in this file
}

using 선언과 지시어

using 선언(using std::cout)은 단일 이름을 가져옵니다—안전하고 권장됩니다. using 지시어(using namespace std)는 모든 것을 가져옵니다—편리하지만 위험합니다, 특히 헤더에서(이름 충돌과 모호성 발생). using 지시어는 작은 스코프(함수/구현 파일)로 제한하고 헤더에서는 절대 사용하지 마세요. C++20 using enum은 범위 지정 enum의 모든 열거자를 가져옵니다.

cpp
#include <iostream>
#include <vector>

// using declaration: brings one name
using std::cout;
using std::vector;
cout << "hello";
vector<int> v;

// using directive: brings entire namespace
using namespace std;  // brings ALL of std
// AVOID in headers! Pollutes global namespace.

// Namespace-scoped using (safe)
namespace mylib {
    using std::string;  // only affects mylib
    using std::vector;
    string s;
}

// C++20: using enum
enum class Color { Red, Green, Blue };
void print() {
    using enum Color;
    auto c = Red;  // no Color:: needed here
}

인수 종속 조회(ADL)

ADL(Koenig 조회)은 인수의 네임스페이스를 기반으로 자유 함수를 찾습니다. 이것이 std::operator<< 없이 std::cout << x가 동작하는 이유입니다—컴파일러가 x의 네임스페이스를 살펴봅니다. ADL은 연산자 오버로드와 swap 같은 사용자 정의 지점에 필수적입니다. 'using std::swap; swap(a,b);' 패턴은 사용자 타입이 최적화된 swap을 제공하면서 std::swap으로 폴백할 수 있게 합니다.

cpp
namespace mylib {
    struct Widget {
        int value;
    };
    // Operator overload found via ADL
    std::ostream& operator<<(std::ostream& os, const Widget& w) {
        return os << "Widget(" << w.value << ")";
    }

    void helper(const Widget&) {}
}

int main() {
    mylib::Widget w{42};
    // ADL: finds operator<< in mylib without qualification
    std::cout << w;  // works! no mylib:: needed

    // ADL applies to free functions too
    helper(w);  // wait—needs mylib::helper unless ADL applies
    mylib::helper(w);  // explicit
}

// swap is the classic ADL use case
namespace ns { struct X {};
    void swap(X&, X&) {}  // customized swap
}
void f(ns::X& a, ns::X& b) {
    using std::swap;
    swap(a, b);  // ADL picks ns::swap if available
}

인라인 네임스페이스(버전 관리)

인라인 네임스페이스는 멤버를 둘러싼 네임스페이스에 있는 것처럼 노출합니다. 이는 라이브러리 버전 관리를 가능하게 합니다: 최신 버전을 인라인으로 만들어 사용자가 기본적으로 받도록 하고, 이전 버전은 명시적 한정으로 접근 가능하게 합니다. ABI 호환성과 기능 토글에도 사용됩니다. 어떤 네임스페이스가 인라인인지 변경하면 사용자 코드 수정 없이 기본 버전이 전환됩니다.

cpp
// Inline namespace members are part of the enclosing namespace
namespace mylib {
    inline namespace v2 {
        struct Widget { int x, y, z; };  // newer version
        void process(Widget) {}
    }

    namespace v1 {
        struct Widget { int x, y; };  // older version
    }
}

// v2::Widget is accessible as mylib::Widget (inline)
mylib::Widget w;  // actually mylib::v2::Widget
mylib::process(w);

// Explicitly use v1 if needed
mylib::v1::Widget oldW;

// ABI versioning: change which namespace is inline
// to change default version without breaking old code

C++20 모듈

C++20 모듈은 #include를 더 빠르고 견고한 시스템으로 대체합니다. export module로 모듈을 선언하고; export로 보이는 선언을 표시합니다. 모듈은 한 번 컴파일되어(번역 단위마다 재파싱되지 않음) 빌드 시간을 획기적으로 개선합니다. 매크로 오염과 헤더 순서 문제를 피합니다. 도입은 점진적입니다—2024년 기준 툴체인 지원(CMake, 빌드 시스템)이 여전히 성숙해지는 중입니다.

cpp
// math.cppm (module interface unit)
export module math;

export double pi = 3.14159;

export double square(double x) {
    return x * x;
}

// Internal (not exported)
double internalHelper(double x) {
    return x * 2;
}

// main.cpp
import math;
import std;  // standard library module (C++23)

int main() {
    return square(pi);  // 9.87
    // internalHelper(3);  // ERROR: not exported
}

// Module partitions (sub-modules)
export module math:geometry;
export double circleArea(double r);
19

전처리기와 매크로

인클루드 가드와 #pragma once

인클루드 가드는 하나의 번역 단위에서 헤더가 여러 번 처리되는 것을 방지하여 재정의 오류를 막습니다. #ifndef/#define/#endif는 표준이고 이식 가능합니다. #pragma once는 더 단순하고 매크로 이름 충돌을 피하지만 기술적으로 비표준입니다(모든 주요 컴파일러에서 지원). 현대 코드는 단순함을 위해 종종 #pragma once를 사용합니다.

cpp
// Traditional include guard (header.h)
#ifndef MY_HEADER_H
#define MY_HEADER_H

// declarations here
class Widget { /* ... */ };

#endif  // MY_HEADER_H

// Alternative: #pragma once (non-standard but widely supported)
#pragma once

class Widget { /* ... */ };

// #pragma once pros: simpler, no risk of macro name collision
// Include guards pros: standard, works everywhere
// Both prevent multiple inclusion in a single TU

함수형 매크로

함수형 매크로는 텍스트 치환입니다. 우선순위 버그를 피하기 위해 각 인수와 전체 표현식을 항상 괄호로 묶으세요. do { ... } while (0) 관용구는 매크로가 단일 문장처럼 동작하게 합니다. 매크로는 타입 검사가 없고, 스코프가 없으며, 부작용이 있을 수 있습니다(MAX(i++, j++)는 두 번 증가). 현대 C++에서는 constexpr/inline/templates를 선호하세요.

cpp
// Basic macro
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define SQUARE(x) ((x) * (x))

int m = MAX(3, 5);      // ((3) > (5) ? (3) : (5))
int s = SQUARE(4);      // ((4) * (4))

// ALWAYS parenthesize arguments!
#define BAD(x) x * x
BAD(1 + 2);  // 1 + 2 * 1 + 2 = 5, not 9!

// Multi-line macro with do-while
#define LOG(msg) do { \
    std::cerr << __FILE__ << ":" << __LINE__ \
              << " " << msg << "\n"; \
} while (0)

// Variadic macro (C++11)
#define PRINT(...) printf(__VA_ARGS__)
PRINT("x=%d, y=%d\n", x, y);

문자열화와 결합

#(문자열화)는 매크로 인수를 문자열 리터럴로 변환합니다. ##(토큰 붙이기)는 두 토큰을 하나로 결합합니다. 두 단계 STR/XSTR 트릭은 먼저 매크로를 확장한 다음 문자열화합니다. 이 연산자들은 코드 생성, 로깅 매크로, X-macros에 필수적입니다. 현대 C++은 종종 constexpr 함수로 이를 대체하지만, 컴파일 타임 문자열/토큰 생성에 여전히 유용합니다.

cpp
// # (stringification) turns a macro argument into a string literal
#define STR(x) #x
#define XSTR(x) STR(x)
const char* s1 = STR(hello world);  // "hello world"
const char* s2 = STR(42);           // "42"

// Two-level macro to expand macros before stringifying
#define VERSION 100
const char* v1 = STR(VERSION);   // "VERSION"
const char* v2 = XSTR(VERSION);  // "100"

// ## (token pasting) concatenates tokens
#define CONCAT(a, b) a ## b
#define MAKE_VAR(n) var_ ## n
int MAKE_VAR(1) = 10;  // int var_1 = 10;
int CONCAT(foo, bar) = 5;  // int foobar = 5;

// Useful for generating unique names
#define UNIQUE(prefix) prefix ## __LINE__
int UNIQUE(tmp_) = 0;  // int tmp_42 = 0; (if on line 42)

조건부 컴파일

조건부 컴파일(#if, #ifdef, #ifndef, #elif, #else, #endif)은 컴파일 타임에 코드를 포함/제외합니다. 플랫폼별 코드, 디버그 빌드, 기능 플래그에 사용됩니다. defined(X)는 X가 정의되었는지 확인합니다(값은 무관). #error는 메시지와 함께 컴파일을 중단합니다. 가능하면 constexpr if를 선호하세요—타입 안전하고 코드가 항상 컴파일됩니다(모든 설정에서 오류를 잡음).

cpp
#define DEBUG 1
#define PLATFORM "windows"

#if DEBUG
    std::cout << "Debug mode\n";
#endif

#ifdef DEBUG
    logDebug("entered function");
#endif

#ifndef NDEBUG
    assert(ptr != nullptr);
#endif

#if defined(_WIN32) || defined(_WIN64)
    #include <windows.h>
#elif defined(__linux__)
    #include <unistd.h>
#elif defined(__APPLE__)
    #include <TargetConditionals.h>
#endif

// Check C++ standard
#if __cplusplus >= 202002L
    #define CPP20_OR_LATER
#elif __cplusplus >= 201703L
    #define CPP17_OR_LATER
#endif

// #error and #warning
#if !defined(VERSION)
    #error "VERSION must be defined"
#endif

미리 정의된 매크로와 __VA_OPT__

미리 정의된 매크로는 컴파일 타임 정보를 제공합니다: __FILE__/__LINE__은 로깅용, __cplusplus는 표준 버전, __func__은 함수 이름(매크로가 아닌 식별자)입니다. __VA_OPT__(C++20)는 __VA_ARGS__가 비어 있지 않을 때만 쉼표를 조건부로 삽입하여 가변 인수 매크로의 후행 쉼표 문제를 해결합니다. __has_include(C++17)는 이식성을 위한 선택적 헤더 포함을 가능하게 합니다.

cpp
// Standard predefined macros
std::cout << __FILE__;       // source file path
std::cout << __LINE__;       // current line number
std::cout << __DATE__;       // compilation date "Mmm dd yyyy"
std::cout << __TIME__;       // compilation time "hh:mm:ss"
std::cout << __cplusplus;    // C++ standard version
std::cout << __func__;       // current function name (not macro)

// Compiler-specific
#ifdef __GNUC__
    std::cout << __GNUC__ << "." << __GNUC_MINOR__;
#endif
#ifdef _MSC_VER
    std::cout << "MSVC " << _MSC_VER;
#endif

// __VA_OPT__ (C++20): expands to its arg if variadic has args
#define LOG(fmt, ...) \
    printf(fmt __VA_OPT__(,) __VA_ARGS__)
LOG("plain");              // printf("plain")
LOG("x=%d", x);            // printf("x=%d", x)

// __has_include (C++17): check if header exists
#if __has_include(<optional>)
    #include <optional>
#endif
20

C++ 디자인 패턴

싱글톤(Meyers의 싱글톤)

싱글톤은 클래스가 전역 접근으로 하나의 인스턴스를 갖도록 보장합니다. Meyers의 싱글톤(정적 지역 변수)은 가장 깔끔한 C++ 구현입니다—C++11부터 스레드 안전 초기화가 보장됩니다. 복제를 방지하기 위해 복사 연산을 삭제하세요. 싱글톤은 논쟁의 여지가 있습니다(전역 상태, 테스트 어려움); 가능하면 의존성 주입을 선호하세요. 진정으로 하나의 인스턴스가 필요할 때(로거, 설정, 하드웨어 인터페이스) 사용하세요.

cpp
class Logger {
public:
    static Logger& instance() {
        // Meyers' singleton: thread-safe in C++11+
        static Logger inst;
        return inst;
    }
    void log(const std::string& msg) {
        std::cout << "[LOG] " << msg << "\n";
    }
    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;
private:
    Logger() = default;
};

// Usage
Logger::instance().log("started");

// Avoid: lazy init with new + manual sync (error-prone)
// The static local is initialized once, thread-safely

RAII(자원 획득은 초기화)

RAII는 C++의 가장 중요한 관용구입니다: 생성자에서 자원을 획득하고 소멸자에서 해제합니다. 이는 예외가 던져져도 정리를 보장합니다. 스마트 포인터, std::lock_guard, std::fstream, std::vector는 모두 RAII를 사용합니다. 소멸자는 스택 되감기 중 실행되어 예외 안전 코드를 자연스럽게 만듭니다. RAII는 수동 new/delete, lock/unlock, open/close를 제거합니다—어디서든 활용하세요.

cpp
// RAII: resource tied to object lifetime
class FileHandle {
    FILE* fp;
public:
    explicit FileHandle(const char* path) : fp(fopen(path, "r")) {
        if (!fp) throw std::runtime_error("open failed");
    }
    ~FileHandle() { if (fp) fclose(fp); }
    FILE* get() { return fp; }
    // disable copy to prevent double-close
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;
    // allow move
    FileHandle(FileHandle&& o) noexcept : fp(o.fp) { o.fp = nullptr; }
};

{
    FileHandle f("data.txt");
    // use f.get()
}  // fclose called automatically, even on exception

Pimpl 관용구(구현에 대한 포인터)

Pimpl(Pointer to Implementation)은 구현 세부 사항을 포인터 뒤에 숨깁니다. 이점: 더 빠른 컴파일(헤더에 private 멤버가 나타나지 않음), 진정한 ABI 안정성(Impl 변경이 ABI를 깨지 않음), 감소된 인클루드 의존성. 소멸자는 unique_ptr<Impl>이 완전한 타입의 Impl을 필요로 하므로 .cpp 파일에 정의해야 합니다. 안정적인 API를 위한 라이브러리 설계에서 흔합니다.

cpp
// widget.h - public interface
class Widget {
public:
    Widget();
    ~Widget();  // must define in .cpp (unique_ptr needs complete type)
    Widget(Widget&&) noexcept;
    Widget& operator=(Widget&&) noexcept;
    void doSomething();
private:
    class Impl;  // forward declaration
    std::unique_ptr<Impl> pimpl;
};

// widget.cpp - implementation
#include "widget.h"
#include <vector>
class Widget::Impl {
public:
    std::vector<int> data;
    void doSomething() { /* ... */ }
};
Widget::Widget() : pimpl(std::make_unique<Impl>()) {}
Widget::~Widget() = default;
Widget::Widget(Widget&&) noexcept = default;
void Widget::doSomething() { pimpl->doSomething(); }

옵저버 패턴

옵저버는 결합 없이 구독자에게 변경 사항을 알립니다. std::function은 람다, 멤버 함수, 함수자를 받기 쉽게 만듭니다. 프로덕션 코드에서는 토큰 기반 구독 해제 메커니즘을 고려하세요(구독 시 ID를 반환하고, 제거에 사용). 관찰자가 [&]를 캡처하고 주체보다 오래 살아남면 매달린 참조를 주의하세요. Qt의 시그널/슬롯과 C#의 이벤트가 성숙한 구현입니다.

cpp
#include <functional>
#include <vector>
#include <string>

class Subject {
    std::vector<std::function<void(const std::string&)>> observers;
public:
    void subscribe(std::function<void(const std::string&)> cb) {
        observers.push_back(cb);
    }
    void notify(const std::string& event) {
        for (auto& cb : observers) cb(event);
    }
};

// Usage
Subject s;
s.subscribe([](const std::string& e){ std::cout << "A: " << e; });
s.subscribe([](const std::string& e){ std::cout << "B: " << e; });
s.notify("hello");  // both callbacks invoked

// For removal, use a token/ID system or observer objects
// instead of std::function

CRTP(Curiously Recurring Template Pattern)

CRTP(Derived : Base<Derived>)는 정적 다형성을 달성합니다—기반 클래스가 파생 타입을 통해 동작을 사용자 정의합니다. 가상 함수 오버헤드가 없습니다. 믹스인을 통해 파생 클래스에 기능(Comparable, Iterable)을 추가하는 데 사용됩니다. 템플릿이 파생 타입을 보장하므로 static_cast를 통한 다운캐스팅이 안전합니다. CRTP는 std::enable_shared_from_this, std::iterator, 많은 정책 기반 설계의 기반입니다.

cpp
// CRTP: class Derived : public Base<Derived>
template <typename Derived>
struct Comparable {
    bool operator==(const Derived& other) const {
        return static_cast<const Derived*>(this)->equalTo(other);
    }
    bool operator!=(const Derived& other) const {
        return !(*this == other);
    }
};

struct Point : Comparable<Point> {
    int x, y;
    bool equalTo(const Point& o) const { return x == o.x && y == o.y; }
};

Point a{1, 2}, b{1, 2};
a == b;  // true (uses Comparable::operator==)
a != b;  // false

// Static polymorphism (no virtual overhead)
template <typename T>
void draw(const T& shape) {
    static_cast<const T&>(shape).drawImpl();
}
21

스마트 포인터 심층

unique_ptr

unique_ptr은 객체의 유일한 소유자입니다. 복사할 수 없고 이동만 가능합니다. 스코프를 벗어나면 자동으로 삭제됩니다. make_unique가 선호되는 생성 방법입니다. 원시 포인터 대비 오버헤드가 없습니다.

cpp
#include <memory>
std::unique_ptr<int> p1 = std::make_unique<int>(42);
// std::unique_ptr<int> p2 = p1;  // Error: cannot copy
std::unique_ptr<int> p3 = std::move(p1);  // OK: transfer ownership
// p1 is now nullptr

shared_ptr

shared_ptr은 참조 카운팅으로 여러 소유자를 허용합니다. use_count()가 소유자 수를 보여줍니다. 카운터에 대해서는 스레드 안전하지만 객체에 대해서는 그렇지 않습니다. 효율성을 위해 make_shared를 사용하세요(단일 할당).

cpp
auto p1 = std::make_shared<int>(42);
auto p2 = p1;  // OK: shared ownership
std::cout << p1.use_count();  // 2
// Reference counting: deleted when count reaches 0
// Thread-safe for reference count, not for the object

weak_ptr

weak_ptr은 shared_ptr에 대한 비소유 참조입니다. 순환 참조(메모리 누수)를 방지합니다. lock()은 shared_ptr로 변환을 시도합니다. expired()로 객체가 여전히 존재하는지 확인하세요. 참조 카운트에 영향을 주지 않습니다.

cpp
auto shared = std::make_shared<int>(42);
std::weak_ptr<int> weak = shared;  // Does not increase count
if (auto locked = weak.lock()) {  // Try to get shared_ptr
    std::cout << *locked;  // Use the object
} else {
    std::cout << "Object deleted";
}

사용자 정의 삭제자

사용자 정의 삭제자는 unique_ptr이 파일 핸들, 소켓, C API 같은 비메모리 자원을 관리하게 합니다. 삭제자 타입은 unique_ptr 타입의 일부입니다. C 라이브러리와의 RAII에 유용합니다.

cpp
std::unique_ptr<FILE, decltype(&fclose)> file(fopen("test.txt", "r"), fclose);
// Or with lambda
auto deleter = [](FILE* f) { if (f) fclose(f); };
std::unique_ptr<FILE, decltype(deleter)> file2(fopen("test.txt", "r"), deleter);

enable_shared_from_this

enable_shared_from_this는 객체가 자신에 대한 shared_ptr을 안전하게 얻을 수 있게 합니다. shared_ptr(this)를 직접 호출하면 두 번째 참조 카운트가 생성되어 이중 삭제를 유발합니다. enable_shared_from_this를 상속하고 shared_from_this()를 사용하세요.

cpp
class Node : public std::enable_shared_from_this<Node> {
public:
    std::shared_ptr<Node> getPtr() {
        return shared_from_this();  // Safe
        // return std::shared_ptr<Node>(this);  // BUG: double delete
    }
};
22

이동 의미론 심층

이동 생성자

이동 생성자는 복사 대신 자원을 빼앗습니다. noexcept가 중요합니다: STL 컨테이너는 이동이 noexcept인 경우에만 이동(복사가 아닌)합니다. 이동된 원본 객체는 항상 유효한 상태로 두세요.

cpp
class Buffer {
    int* data; size_t size;
public:
    Buffer(Buffer&& other) noexcept : data(other.data), size(other.size) {
        other.data = nullptr; other.size = 0;  // Leave in valid state
    }
    Buffer& operator=(Buffer&& other) noexcept {
        delete[] data;
        data = other.data; size = other.size;
        other.data = nullptr; other.size = 0;
        return *this;
    }
};

std::move

std::move는 rvalue 참조로의 캐스팅입니다. 이동 자체를 수행하지 않습니다. 이동 생성자나 대입 연산자가 실제 작업을 수행합니다. std::move 이후 원본 객체는 유효하지만 지정되지 않은 상태입니다.

cpp
std::string s1 = "Hello";
std::string s2 = std::move(s1);  // s1 is now empty
// std::move does not move anything!
// It casts to an rvalue reference, enabling move
// The actual move happens in the constructor/assignment

완벽한 전달

완벽한 전달은 인수의 값 카테고리를 보존합니다. 템플릿에서 T&&는 전달 참조로, lvalue와 rvalue 모두에 바인딩됩니다. std::forward<T>는 원래 카테고리로 다시 캐스팅합니다. 팩토리 함수와 래퍼에 필수적입니다.

cpp
template<typename T, typename Arg>
auto make(Arg&& arg) {
    return std::make_shared<T>(std::forward<Arg>(arg));
}
// std::forward preserves value category:
// lvalue stays lvalue, rvalue stays rvalue
// Arg&& is a forwarding reference (not rvalue reference)

RVO와 NRVO

RVO(반환 값 최적화)와 NRVO(이름 있는 RVO)는 객체를 제자리에 생성하여 복사를 제거합니다. 컴파일러가 자동으로 수행합니다. 지역 반환 값에 std::move를 사용하면 NRVO를 방해하고 성능이 저하될 수 있습니다. 컴파일러를 신뢰하세요.

cpp
std::string create() {
    std::string s = "Hello";
    return s;  // NRVO: no copy, no move
}
std::string s = create();  // RVO: no copy
// With -O2, compilers eliminate the copy/move entirely
// Do NOT use std::move on return of local variable!

5의 법칙

5의 법칙: 소멸자, 복사 생성자/대입, 또는 이동 생성자/대입 중 하나를 정의한다면 다섯 개 모두 정의하세요. 이는 올바른 자원 관리를 보장합니다. 0의 법칙이 선호됩니다: 수동 관리를 피하기 위해 RAII 타입(스마트 포인터, vector)을 사용하세요.

cpp
class Resource {
public:
    Resource();  // Constructor
    ~Resource();  // Destructor
    Resource(const Resource&);  // Copy constructor
    Resource& operator=(const Resource&);  // Copy assignment
    Resource(Resource&&) noexcept;  // Move constructor
    Resource& operator=(Resource&&) noexcept;  // Move assignment
};
23

템플릿 심층

가변 템플릿

가변 템플릿은 임의의 개수의 인수를 받습니다. 폴드 표현식(C++17)이 언팩을 단순화합니다. C++11에서는 재귀적 접근이 동작합니다. sizeof...(args)가 개수를 제공합니다. tuple, make_shared, printf 대체에 사용됩니다.

cpp
template<typename... Args>
void print(Args... args) {
    (std::cout << ... << args) << '\n';  // C++17 fold expression
}
print(1, "hello", 3.14, 'a');  // 1hello3.14a
// Recursive unpacking (pre-C++17)
template<typename T, typename... Rest>
void print(T first, Rest... rest) {
    std::cout << first;
    print(rest...);
}

SFINAE

SFINAE(Substitution Failure Is Not An Error)는 잘못된 오버로드를 고려에서 제거합니다. enable_if는 템플릿에 조건을 추가합니다. 조건이 거짓이면 특수화가 무시됩니다. C++17의 if constexpr이 종종 더 깔끔합니다.

cpp
template<typename T,
         typename = std::enable_if_t<std::is_integral_v<T>>>
void process(T value) { /* integer version */ }
template<typename T,
         typename = std::enable_if_t<std::is_floating_point_v<T>>,
         typename = void>  // Different signature
void process(T value) { /* float version */ }

if constexpr

if constexpr(C++17)는 컴파일 타임에 조건을 평가합니다. 참인 분기만 컴파일되고 다른 하나는 버려집니다. 조건부 컴파일에 SFINAE보다 훨씬 깔끔합니다. auto 반환 타입 연역과 함께 동작합니다.

cpp
template<typename T>
auto get_value(T t) {
    if constexpr (std::is_pointer_v<T>)
        return *t;
    else
        return t;
}
// Only the matching branch is compiled
// No SFINAE needed

컨셉트(C++20)

컨셉트(C++20)는 가독성 좋은 구문으로 템플릿 매개변수를 제약합니다. SFINAE보다 나은 오류 메시지를 제공합니다. 기존 컨셉트(integral, floating_point)를 사용하거나 사용자 정의를 정의하세요. 컨셉트는 &&와 ||로 결합할 수 있습니다.

cpp
template<typename T>
concept Number = std::integral<T> || std::floating_point<T>;
template<Number T>
T add(T a, T b) { return a + b; }
// Or: requires clause
template<typename T> requires Number<T>
T multiply(T a, T b) { return a * b; }

템플릿 특수화

완전 특수화는 특정 타입에 대한 완전한 구현을 제공합니다. 부분 특수화는 카테고리(예: 모든 포인터)에 맞게 사용자 정의합니다. 기본 템플릿을 먼저 선언해야 합니다. 특수화는 인터페이스와 일치해야 합니다.

cpp
template<typename T>
class Vector { /* general implementation */ };
template<>
class Vector<bool> {  // Full specialization
    // Bit-packed implementation
};
template<typename T>
class Vector<T*> {  // Partial specialization for pointers
    // Pointer-specific implementation
};
24

STL 알고리즘 심층

sort와 stable_sort

sort는 O(n log n)이며 안정적이지 않습니다. stable_sort는 동등한 요소의 상대적 순서를 보존합니다. 사용자 정의 순서를 위해 비교자를 사용하세요. C++20 ranges는 std::ranges::sort(v, {}, &last_digit)을 허용합니다.

cpp
std::vector<int> v = {3, 1, 4, 1, 5, 9};
std::sort(v.begin(), v.end());  // 1,1,3,4,5,9
std::sort(v.begin(), v.end(), std::greater<>());  // Descending
// Custom comparator
std::sort(v.begin(), v.end(), [](int a, int b) {
    return a % 10 < b % 10;  // Sort by last digit
});

transform과 accumulate

transform은 각 요소에 함수를 적용합니다. accumulate는 연산(기본값 +)으로 요소를 접습니다. 초기 값이 타입을 결정합니다. 병렬 감소를 위해 std::reduce(C++17)를 사용하세요.

cpp
std::vector<int> v = {1, 2, 3, 4};
std::vector<int> squared(v.size());
std::transform(v.begin(), v.end(), squared.begin(),
    [](int x) { return x * x; });  // 1,4,9,16
int sum = std::accumulate(v.begin(), v.end(), 0);  // 10
int product = std::accumulate(v.begin(), v.end(), 1, std::multiplies<>());

find와 count

find는 첫 번째 일치 항목 또는 end()에 대한 반복자를 반환합니다. count는 일치 항목 수를 반환합니다. find_if는 술어를 사용합니다. 모두 O(n)입니다. 정렬된 범위에서는 binary_search, lower_bound, upper_bound(O(log n))를 사용하세요.

cpp
std::vector<int> v = {1, 2, 3, 2, 1};
auto it = std::find(v.begin(), v.end(), 2);  // First 2
size_t cnt = std::count(v.begin(), v.end(), 2);  // 2
auto it2 = std::find_if(v.begin(), v.end(),
    [](int x) { return x > 2; });  // First > 2

copy와 remove

copy_if는 일치하는 요소를 복사합니다. back_inserter는 목적지에 추가합니다. remove는 실제로 제거하지 않고; 일치하지 않는 요소를 앞으로 이동시키고 새 끝을 반환합니다. erase가 나머지를 제거합니다. C++20은 컨테이너에 std::erase를 추가합니다.

cpp
std::vector<int> v = {1, 2, 3, 4, 5};
std::vector<int> evens;
std::copy_if(v.begin(), v.end(), std::back_inserter(evens),
    [](int x) { return x % 2 == 0; });  // 2,4
// Remove-erase idiom
v.erase(std::remove(v.begin(), v.end(), 3), v.end());
// C++20: std::erase(v, 3);

minmax와 clamp

minmax는 한 번의 순회로 (min, max) 쌍을 반환합니다. clamp는 값을 범위로 제한합니다. min/max는 초기화 목록이나 두 값을 받습니다. 모두 사용자 정의 비교자를 받습니다. 구조화 바인딩(C++17)이 결과를 단순화합니다.

cpp
auto [min, max] = std::minmax({3, 1, 4, 1, 5});  // C++17
int value = std::clamp(15, 0, 10);  // 10 (clamped to max)
int m = std::min({1, 2, 3});
int m2 = std::max({1, 2, 3}, [](int a, int b) { return a < b; });
25

동시성

std::thread

std::thread는 OS 스레드를 생성합니다. join()은 완료를 대기합니다(차단). detach()는 독립적으로 실행합니다(생성자보다 오래 살 수 있음). 둘 다 없는 스레드는 std::terminate를 유발합니다. 인수는 값으로 전달하거나 참조에는 std::ref를 사용하세요.

cpp
#include <thread>
void task(int n) { /* ... */ }
std::thread t1(task, 42);
std::thread t2([]() { /* lambda */ });
t1.join();  // Wait for completion
t2.detach();  // Run independently
// Always join or detach before destruction

std::mutex

lock_guard는 단순한 RAII입니다: 생성 시 잠그고 소멸 시 해제합니다. unique_lock은 더 유연합니다: unlock/relock 가능, 조건 변수와 사용. lock_guard로 절대 수동으로 unlock하지 마세요. 여러 뮤텍스에는 std::scoped_lock을 사용하세요.

cpp
std::mutex mtx;
int shared = 0;
void increment() {
    std::lock_guard<std::mutex> lock(mtx);  // RAII
    ++shared;
}  // Auto-unlock
// std::unique_lock for conditional locking
std::unique_lock<std::mutex> ulock(mtx);
ulock.unlock();  // Manual unlock
ulock.lock();    // Re-lock

std::async와 future

std::async는 함수를 비동기적으로 실행하고 future를 반환합니다. get()은 차단하고 결과를 가져옵니다. launch::async는 새 스레드를 강제합니다. launch::deferred는 get()에서 동기적으로 실행합니다. 기본값은 둘 중 하나일 수 있습니다. 예외는 get()을 통해 전파됩니다.

cpp
#include <future>
std::future<int> f = std::async(std::launch::async, []() {
    return 42;
});
int result = f.get();  // Blocks until ready
// std::launch::async: always new thread
// std::launch::deferred: lazy, runs on get()

조건 변수

condition_variable은 스레드를 동기화합니다. wait는 잠금을 해제하고 알림을 받을 때까지 차단합니다. 술어가 거짓 깨움을 처리합니다. notify_one은 하나의 대기자를 깨우고, notify_all은 모두 깨웁니다. 알림 전에 항상 뮤텍스 하에서 공유 상태를 수정하세요.

cpp
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
// Waiter
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []() { return ready; });  // Spurious wakeups handled
// Notifier
{
    std::lock_guard<std::mutex> lk(mtx);
    ready = true;
}
cv.notify_one();  // Or notify_all()

원자적 연산

atomic은 잠금 없는 스레드 안전 연산을 제공합니다. fetch_add/sub는 읽기-수정-쓰기입니다. compare_exchange는 CAS(compare-and-swap)를 구현합니다. memory_order_relaxed가 가장 빠르지만 가장 약합니다. seq_cst(기본값)가 가장 강합니다. 단순한 카운터와 플래그에는 원자적 연산을 사용하세요.

cpp
#include <atomic>
std::atomic<int> counter{0};
counter++;  // Atomic increment
counter.fetch_add(1, std::memory_order_relaxed);
bool expected = false;
counter.compare_exchange_strong(expected, true);
// Memory orders: relaxed, acquire, release, seq_cst
26

현대 C++ 기능

구조화 바인딩

구조화 바인딩(C++17)은 pair, tuple, struct를 분해합니다. 참조에는 auto&, 복사에는 auto. 맵 순회를 단순화합니다. 모든 집계 타입과 동작합니다. .first/.second보다 훨씬 깔끔합니다.

cpp
std::pair p = {1, "hello"};
auto [num, str] = p;  // C++17
std::map<std::string, int> m = {{"a", 1}, {"b", 2}};
for (const auto& [key, value] : m) {
    std::cout << key << ": " << value;
}

std::optional

optional은 존재할 수도 있고 아닐 수도 있는 값을 나타냅니다. 포인터나 센티널 값보다 낫습니다. has_value() 또는 operator bool로 확인합니다. value()는 비어 있으면 예외를 던지고, value_or()는 기본값을 제공합니다. 결과 생성에 실패할 수 있는 함수에 사용하세요.

cpp
#include <optional>
std::optional<int> find(bool found) {
    if (found) return 42;
    return std::nullopt;
}
auto result = find(true);
if (result) std::cout << *result;
// Or: result.value_or(0)

std::variant

variant는 타입 안전한 공용체입니다. 여러 타입 중 하나를 보유합니다. visit은 방문자(오버로드된 람다)를 적용합니다. index()는 현재 타입 인덱스를 반환합니다. get_if<T>로 안전하게 가져옵니다. 닫힌 타입 계층에 대해 공용체와 상속을 대체합니다.

cpp
#include <variant>
std::variant<int, std::string> v;
v = 42;
v = "hello";
std::visit([](auto&& arg) {
    using T = std::decay_t<decltype(arg)>;
    if constexpr (std::is_same_v<T, int>)
        std::cout << "int: " << arg;
    else if constexpr (std::is_same_v<T, std::string>)
        std::cout << "string: " << arg;
}, v);

Ranges(C++20)

Ranges(C++20)는 조합 가능하고 지연된 뷰를 제공합니다. | 연산자가 연산을 연결합니다. 중간 컨테이너가 없습니다. 뷰는 지연됩니다: 계산은 순회 시 발생합니다. 중첩된 알고리즘 호출보다 훨씬 가독성이 좋습니다.

cpp
#include <ranges>
namespace rv = std::ranges::views;
auto result = std::vector{1, 2, 3, 4, 5}
    | rv::filter([](int x) { return x % 2 == 0; })
    | rv::transform([](int x) { return x * x; });
// Lazy evaluation: no computation until iterated
for (int x : result) std::cout << x;  // 4, 16

코루틴(C++20)

코루틴(C++20)은 비동기와 생성기 패턴을 가능하게 합니다. co_yield는 일시 중지하고 값을 반환합니다. co_await는 다른 코루틴을 기다립니다. co_return은 종료합니다. 컴파일러는 코루틴을 상태 기계로 변환합니다. 반환 타입(Generator, Task) 구현이 필요합니다.

cpp
#include <coroutine>
Generator<int> counter() {
    for (int i = 0; ; ++i)
        co_yield i;  // Suspend and yield
}
for (int x : counter()) {
    if (x > 5) break;
    std::cout << x;
}
// co_await: wait for async operation
// co_return: finish coroutine
27

일반적인 함정

매달린 포인터

매달린 포인터는 해제된 메모리를 가리킵니다. 접근하면 정의되지 않은 동작입니다. 수동 메모리 관리를 피하기 위해 스마트 포인터(unique_ptr, shared_ptr)를 사용하세요. 원시 포인터를 사용하는 경우 delete 후 nullptr로 설정하세요. 감지를 위해 AddressSanitizer 같은 도구를 사용하세요.

cpp
// BUG: dangling pointer
int* p = new int(42);
delete p;
std::cout << *p;  // Undefined behavior
// FIX: use smart pointers
auto p = std::make_unique<int>(42);
// Or set to nullptr after delete
int* p2 = new int(42);
delete p2; p2 = nullptr;

반복자 무효화

Vector의 push_back은 재할당하여 모든 반복자를 무효화할 수 있습니다. erase는 해당 지점과 그 이후의 반복자를 무효화합니다. list의 반복자는 제거된 요소를 제외하고 안정적입니다. 각 컨테이너의 반복자 무효화 규칙을 확인하세요. 의심스러울 때는 인덱스를 사용하세요.

cpp
std::vector<int> v = {1, 2, 3};
// BUG: iterator invalidated
for (auto it = v.begin(); it != v.end(); ++it) {
    if (*it == 2) v.push_back(4);  // May invalidate it
}
// FIX: use index
for (size_t i = 0; i < v.size(); ++i) {
    if (v[i] == 2) v.push_back(4);
}

정의되지 않은 동작

정의되지 않은 동작(UB)은 컴파일러가 무엇이든 하도록 허용합니다. 초기화되지 않은 변수, 범위를 벗어난 접근, null 역참조, 부호 있는 오버플로우는 모두 UB입니다. 컴파일러는 UB가 없다고 가정하고 최적화하여 놀라운 결과를 초래합니다. 감지를 위해 -fsanitize=undefined를 사용하세요.

cpp
int x;  // Uninitialized: UB to read
std::cout << x;  // UB
int arr[5];
arr[5] = 0;  // Out of bounds: UB
int* p = nullptr;
*p;  // Null dereference: UB
// Signed overflow: UB (unsigned wraps)
int big = INT_MAX + 1;  // UB

슬라이싱

객체 슬라이싱은 파생 객체가 기반 타입으로 복사될 때 발생합니다. 파생 부분이 손실됩니다. 슬라이싱을 피하기 위해 참조나 포인터로 전달하세요. 슬라이스된 객체의 가상 호출은 기반 버전을 호출합니다. 다형적 타입은 항상 참조/포인터로 전달하세요.

cpp
class Base { public: virtual ~Base() {} };
class Derived : public Base { int extra; };
void take(Base b) { /* copies Base part only */ }
Derived d;
take(d);  // Sliced: extra lost
// FIX: pass by reference or pointer
void take(const Base& b) { /* full object */ }

정적 초기화

번역 단위 간의 정적 초기화 순서는 정의되지 않습니다. 한 파일이 아직 초기화되지 않은 다른 파일의 정적 변수를 사용할 수 있습니다. 함수 지역 정적(Meyers 싱글톤)으로 수정하세요: 초기화는 지연되고 스레드 안전합니다(C++11+). 함수 호출을 통해 접근하세요.

cpp
// Static initialization order fiasco
// File1.cpp
int x = computeX();  // May use y
// File2.cpp
int y = computeY();  // May use x
// Order across files is undefined!
// FIX: function-local static
int& getX() {
    static int x = computeX();  // Lazy, thread-safe
    return x;
}

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.