출시·고도화 중
C++ 안내서 · 6/6
C++를 C++답게 쓰려면 몇 가지 핵심 개념을 알아야 합니다. 이 장에서는 자원 관리의 기본 원칙인 RAII, 이를 바탕으로 한 스마트 포인터, 불필요한 복사를 없애는 이동 의미론, 그리고 타입에 상관없이 동작하는 코드를 쓰는 템플릿을 다룹니다.
RAII(Resource Acquisition Is Initialization)는 "자원은 객체가 생성될 때 얻고, 객체가 소멸할 때 돌려준다"는 원칙입니다. C++에서 지역 객체의 소멸자는 함수가 정상적으로 끝나든 예외로 빠져나가든 반드시 호출되므로, 메모리·파일·잠금 같은 자원을 객체에 맡기면 해제를 잊을 수 없습니다.
#include <cstdio>
#include <stdexcept>
class File {
public:
File(const char* path, const char* mode) : handle_{std::fopen(path, mode)} {
if (!handle_) throw std::runtime_error("파일을 열 수 없습니다");
}
~File() { std::fclose(handle_); } // 범위를 벗어나면 자동으로 닫힘
File(const File&) = delete; // 복사 금지: 같은 핸들을 두 번 닫지 않도록
File& operator=(const File&) = delete;
std::FILE* get() const noexcept { return handle_; }
private:
std::FILE* handle_;
};
void write_log() {
File f{"log.txt", "w"};
std::fputs("start\n", f.get());
// 여기서 예외가 나도 f 의 소멸자가 파일을 닫습니다
}표준 라이브러리도 같은 원칙을 따릅니다. std::vector와 std::string은 메모리를, std::ifstream은 파일을, std::lock_guard와 std::scoped_lock은 뮤텍스 잠금을 관리합니다. 현대 C++ 코드에서는 new/delete를 직접 쓸 일이 거의 없습니다.
동적으로 만든 객체의 수명은 <memory>의 스마트 포인터로 관리합니다.
| 타입 | 소유 방식 | 만드는 함수 |
|---|---|---|
std::unique_ptr<T> | 단독 소유. 복사 불가, 이동 가능 | std::make_unique<T>(...) (C++14) |
std::shared_ptr<T> | 공동 소유. 참조 횟수가 0이 되면 해제 | std::make_shared<T>(...) |
std::weak_ptr<T> | 소유하지 않고 관찰만. 순환 참조를 끊을 때 사용 | shared_ptr에서 만듦 |
#include <iostream>
#include <memory>
#include <string>
#include <vector>
struct Shape {
virtual ~Shape() = default;
virtual double area() const = 0;
};
struct Circle : Shape {
explicit Circle(double r) : r{r} {}
double area() const override { return 3.14159 * r * r; }
double r;
};
int main() {
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(1.0));
shapes.push_back(std::make_unique<Circle>(2.0));
for (const auto& s : shapes) std::cout << s->area() << '\n';
// shapes 가 사라질 때 모든 Circle 이 자동으로 해제됩니다
auto config = std::make_shared<std::string>("debug");
auto another = config; // 참조 횟수 2
std::cout << config.use_count() << '\n'; // 2
}기본 선택은 std::unique_ptr입니다. 정말로 여러 곳이 수명을 함께 책임져야 할 때만 std::shared_ptr를 씁니다. 소유권과 상관없이 객체를 잠깐 쓰기만 하는 함수는 스마트 포인터 대신 참조(const Shape&)나 일반 포인터를 받습니다.
C++11은 복사 대신 자원을 "옮기는" 이동 연산을 도입했습니다. 곧 사라질 임시 객체나 std::move로 표시한 객체는 내부 버퍼를 통째로 넘겨줄 수 있어 큰 복사를 피할 수 있습니다. std::move 자체는 아무것도 옮기지 않고, 이동해도 된다는 표시(rvalue로 캐스팅)만 합니다.
#include <iostream>
#include <string>
#include <utility>
#include <vector>
int main() {
std::string big(1'000'000, 'x');
std::vector<std::string> list;
list.push_back(big); // 복사: big 은 그대로
list.push_back(std::move(big)); // 이동: 버퍼를 넘겨줌
// big 은 "유효하지만 지정되지 않은" 상태. 새 값을 대입하기 전에는 내용을 기대하지 않습니다
big = "reused";
std::cout << list.size() << '\n'; // 2
}함수에서 지역 객체를 값으로 돌려줄 때는 return std::move(x);라고 쓰지 않습니다. 컴파일러가 복사 생략(copy elision)이나 자동 이동을 적용하므로 그냥 return x;가 가장 좋습니다. 또 클래스가 자원을 직접 관리하지 않는다면 복사·이동 연산을 직접 정의하지 않고 컴파일러가 만들어 주는 것을 쓰는 편이 좋습니다(Rule of Zero).
템플릿은 타입을 매개변수로 받는 함수나 클래스를 만듭니다. 컴파일러는 실제로 쓰인 타입마다 코드를 만들어 냅니다. 표준 컨테이너와 알고리즘이 모두 템플릿입니다.
#include <concepts>
#include <iostream>
#include <string>
#include <utility>
#include <vector>
template <typename T>
T max_of(const T& a, const T& b) {
return (a < b) ? b : a;
}
template <typename T>
class Stack {
public:
void push(T value) { items_.push_back(std::move(value)); }
T pop() { T top = std::move(items_.back()); items_.pop_back(); return top; }
bool empty() const { return items_.empty(); }
private:
std::vector<T> items_;
};
// C++20 concepts: 정수 타입만 받도록 제약
template <std::integral T>
T half(T value) { return value / 2; }
int main() {
std::cout << max_of(3, 7) << '\n'; // 7
std::cout << max_of(std::string{"a"}, std::string{"b"}) << '\n'; // b
Stack<int> s;
s.push(1);
s.push(2);
std::cout << s.pop() << ' ' << half(10) << '\n'; // 2 5
// half(1.5); // 오류: double 은 std::integral 을 만족하지 않습니다
}템플릿 정의는 사용하는 곳에서 보여야 하므로 보통 헤더에 둡니다. C++20 concepts를 쓰면 템플릿 인자에 대한 요구 사항을 이름으로 표현할 수 있고, 조건을 어겼을 때 오류 메시지도 훨씬 읽기 쉬워집니다.
std::make_unique로 만들고, 공동 소유가 꼭 필요할 때만 std::shared_ptr를 씁니다.std::move는 이동을 허락하는 표시이며, 이동된 객체에는 새 값을 대입하기 전까지 기대지 않습니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.
첫 댓글을 남겨 보세요.