C++ shared_ptr 고급 완벽 가이드 | enable_shared_from_this·aliasing
이 글의 핵심
this를 shared_ptr로 감싸다 이중 해제로 크래시한 적 있나요? enable_shared_from_this, aliasing 생성자, shared_ptr 스레드 안전성, 멤버 포인터 수명 관리까지. 실전 문제 시나리오와 프로덕션 패턴.
들어가며: this를 shared_ptr로 감싸다 크래시한 적 있나요?
”비동기 콜백에서 this를 쓰다 use-after-free가 났어요”
shared_ptr 기초를 익힌 뒤, 실전에서는 this를 shared_ptr로 감싸서 비동기 콜백에 넘기거나, 부모 객체의 멤버 포인터를 shared_ptr로 반환하는 상황이 자주 발생합니다. 이때 잘못된 패턴을 쓰면 이중 해제(double-free)나 use-after-free로 크래시가 납니다.
비유하면: “내 집 열쇠를 복제해서 친구에게 주려는데, 원본 열쇠와 복제 열쇠가 서로 다른 관리 시스템을 쓰면, 한쪽이 문을 닫았다고 해서 다른 쪽이 또 닫으려다 충돌하는 것”과 같습니다. enable_shared_from_this는 “이미 shared_ptr로 관리 중인 객체”에서 안전하게 shared_ptr을 얻는 방법을 제공합니다.
이 글을 읽으면:
enable_shared_from_this로 this를 안전하게 shared_ptr로 변환할 수 있습니다.- aliasing 생성자로 부모 수명에 묶인 멤버 포인터를 반환할 수 있습니다.
- 커스텀 삭제자로 FILE*, 배열 등 delete가 아닌 리소스를 관리할 수 있습니다.
atomic<shared_ptr>과 weak_ptr 옵저버 패턴으로 멀티스레드·이벤트 시스템을 구현할 수 있습니다.- shared_ptr의 스레드 안전성 범위를 정확히 이해할 수 있습니다.
- 자주 하는 실수와 프로덕션 패턴을 익힐 수 있습니다.
목차
- 문제 시나리오
- enable_shared_from_this 완전 가이드
- aliasing 생성자
- 커스텀 삭제자 (Custom Deleter)
- shared_ptr 스레드 안전성
- 완전한 예제 코드
- 자주 발생하는 에러와 해결법
- 모범 사례
- 프로덕션 패턴
- 성능과 체크리스트
1. 문제 시나리오
시나리오 1: “비동기 콜백에서 this를 캡처했다가 use-after-free예요”
"네트워크 요청 완료 시 this->onComplete()를 호출하는데,
요청이 끝나기 전에 객체가 삭제되면 크래시해요."
상황: AsyncService가 비동기 API를 호출하고, 완료 시 this를 람다에 캡처합니다. 호출자가 AsyncService를 스택에 두고 함수를 반환하면 객체가 파괴되는데, 비동기 콜백은 아직 대기 중입니다. 콜백이 실행될 때 이미 해제된 this를 사용해 use-after-free가 발생합니다.
해결 포인트: enable_shared_from_this를 상속하고, 콜백에 shared_from_this()를 넘깁니다. shared_ptr이 하나라도 남아 있는 한 객체가 해제되지 않습니다.
시나리오 2: “부모 객체의 멤버를 shared_ptr로 반환하고 싶어요”
"Container가 Bar 멤버를 가지고 있는데,
shared_ptr<Bar>를 반환하면서 Container 수명도 유지되게 하고 싶어요."
상황: shared_ptr<Container>가 있고, Container의 멤버 Bar*를 shared_ptr<Bar>로 반환해야 합니다. shared_ptr<Bar>(container->member)처럼 만들면 새로운 제어 블록이 생겨, Container와 Bar의 수명이 분리됩니다. Container가 먼저 삭제되면 Bar는 이미 해제된 메모리를 가리키게 됩니다.
해결 포인트: aliasing 생성자 shared_ptr<Bar>(container, &container->member)를 사용하면, Bar는 Container의 수명에 묶입니다. Container가 살아 있는 한 Bar에 안전하게 접근할 수 있습니다.
시나리오 3: “여러 스레드가 shared_ptr을 공유하는데 데이터 레이스가 나요”
"스레드 A와 B가 같은 shared_ptr을 동시에 reset하면 크래시해요."
"참조 카운트는 스레드 안전한데, 왜 문제가 생기나요?"
상황: shared_ptr의 참조 카운트 증감은 원자적이지만, shared_ptr 객체 자체를 여러 스레드가 동시에 수정(복사 대입, reset 등)하면 데이터 레이스가 발생합니다. 같은 shared_ptr 변수를 여러 스레드가 공유하면서 수정하는 것이 문제입니다.
해결 포인트: 각 스레드가 자기만의 복사본을 가지거나, std::atomic<std::shared_ptr<T>>(C++20)를 사용합니다.
시나리오 4: “shared_ptr로 생성되지 않은 객체에서 shared_from_this()를 호출했어요”
"스택에 선언한 객체에서 shared_from_this()를 호출했더니
std::bad_weak_ptr 예외가 났어요."
상황: enable_shared_from_this를 상속했어도, 객체가 shared_ptr로 생성되지 않으면 내부 weak_ptr이 초기화되지 않습니다. shared_from_this()는 이때 std::bad_weak_ptr을 던집니다.
해결 포인트: enable_shared_from_this를 쓰는 클래스는 반드시 std::make_shared<T>() 또는 shared_ptr<T>(new T(...))로 생성합니다. 스택에 두지 않습니다.
시나리오 5: “생성자에서 shared_from_this()를 호출했어요”
"생성자에서 shared_from_this()를 호출했더니
아직 shared_ptr이 없어서 예외가 나요."
상황: 생성자가 실행되는 시점에는 아직 shared_ptr이 객체를 소유하지 않습니다. enable_shared_from_this의 내부 weak_ptr도 아직 설정되지 않아, shared_from_this()를 호출하면 std::bad_weak_ptr이 발생합니다.
해결 포인트: shared_from_this()는 생성자와 소멸자 밖에서만 호출합니다. 초기화가 필요한 경우 void init() 같은 별도 메서드에서 호출합니다.
시나리오 6: “FILE*나 배열을 shared_ptr로 관리하고 싶어요”
"fopen()으로 연 파일을 shared_ptr이 해제될 때 fclose()로 닫고 싶어요."
"new[]로 할당한 배열을 shared_ptr이 delete[]로 해제하게 하고 싶어요."
상황: shared_ptr은 기본적으로 delete ptr을 호출합니다. FILE*, int[](배열), HANDLE, pthread_mutex_t* 등 delete가 아닌 다른 해제 방식이 필요한 리소스는 기본 shared_ptr로 관리할 수 없습니다. 잘못 사용하면 delete 대신 fclose()가 호출되어야 하는데 delete가 호출되거나, delete 대신 delete[]가 필요한데 delete만 호출되는 미정의 동작이 발생합니다.
해결 포인트: 커스텀 삭제자(Custom Deleter)를 두 번째 인자로 전달합니다. shared_ptr<T>(ptr, deleter) 형태로, deleter는 void(ptr) 시그니처의 호출 가능 객체입니다.
시나리오 7: “옵저버가 주제를 참조하다 use-after-free가 나요”
"이벤트 구독자 목록에 shared_ptr로 등록했더니,
구독 해제해도 발행자가 shared_ptr을 들고 있어서 객체가 안 죽어요."
"반대로 weak_ptr을 쓰면, 접근할 때 이미 삭제됐을 수 있어요."
상황: 옵저버 패턴에서 발행자(Subject)가 구독자(Observer) 목록을 vector<shared_ptr<Observer>>로 들면, 구독자가 자기 자신을 해제해도 발행자가 shared_ptr을 유지해 객체가 영원히 살아 있습니다. 반대로 vector<Observer*>로 raw 포인터를 쓰면, 구독자가 먼저 삭제된 뒤 발행자가 콜백을 호출할 때 use-after-free가 발생합니다.
해결 포인트: 발행자는 vector<weak_ptr<Observer>>로 구독자를 저장합니다. 콜백 호출 전에 lock()으로 shared_ptr을 얻고, expired()이면 건너뜁니다. “있으면 쓰고, 없으면 무시”하는 weak_ptr 옵저버 패턴입니다.
2. enable_shared_from_this 완전 가이드
enable_shared_from_this란?
std::enable_shared_from_this<T>를 상속하면, 이미 shared_ptr로 관리 중인 객체에서 shared_from_this()를 통해 동일한 제어 블록을 공유하는 shared_ptr을 얻을 수 있습니다. shared_ptr<T>(this)처럼 새 shared_ptr을 만들면 새 제어 블록이 생겨 이중 해제가 발생합니다.
왜 shared_ptr(this)가 위험한가?
flowchart TB
subgraph bad["❌ shared_ptr(this) 사용"]
B1["shared_ptr A"] --> B2["제어 블록 1"]
B3["shared_ptr B = shared_ptr(this)"] --> B4["제어 블록 2"]
B2 --> B5["객체"]
B4 --> B5
B5 -.->|"각각 독립적으로 delete 시도 → 이중 해제!"| B5
end
주의사항: shared_ptr를 this로 만드는 패턴은 enable_shared_from_this 없이 쓰면 거의 항상 잘못된 설계입니다.
// ❌ 위험한 코드: 이중 해제
class BadService {
public:
std::shared_ptr<BadService> getShared() {
return std::shared_ptr<BadService>(this); // 새 제어 블록!
}
};
int main() {
auto p1 = std::make_shared<BadService>();
auto p2 = p1->getShared(); // p1과 p2는 서로 다른 제어 블록
// p1 소멸 시 객체 delete
// p2 소멸 시 같은 객체를 또 delete → 이중 해제 크래시!
return 0;
}
코드 설명:
p1은make_shared로 생성되어 제어 블록 1을 가집니다.getShared()가shared_ptr(this)를 반환하면 제어 블록 2가 새로 만들어집니다.- 두 shared_ptr이 같은 객체를 가리키지만 서로 다른 참조 카운트를 가져, 각각 0이 될 때마다 delete를 시도합니다.
- 결과: 이중 해제(double-free)로 크래시.
enable_shared_from_this 사용법
#include <memory>
#include <iostream>
class GoodService : public std::enable_shared_from_this<GoodService> {
public:
std::shared_ptr<GoodService> getShared() {
return shared_from_this(); // ✅ 동일 제어 블록 공유
}
void doWork() { std::cout << "Working\n"; }
};
int main() {
auto p1 = std::make_shared<GoodService>();
auto p2 = p1->getShared(); // p1과 p2는 같은 제어 블록
std::cout << "use_count: " << p1.use_count() << std::endl; // 2
return 0; // p2, p1 순으로 소멸 → 한 번만 delete
}
주의사항: 스택에 둔 객체에서 shared_from_this()를 호출하면 bad_weak_ptr 예외가 나므로, 반드시 make_shared 등으로 힙에 둔 뒤에만 씁니다.
코드 설명:
shared_from_this()는 객체가 이미 shared_ptr로 관리될 때만 호출 가능합니다.- 반환된 shared_ptr은 원본과 같은 제어 블록을 공유하므로 참조 카운트가 올바르게 동작합니다.
- 마지막 shared_ptr이 소멸될 때 한 번만 delete됩니다.
비동기 콜백에서의 완전한 예제
#include <memory>
#include <functional>
#include <iostream>
#include <thread>
#include <chrono>
class AsyncWorker : public std::enable_shared_from_this<AsyncWorker> {
public:
void startAsync(std::function<void()> onComplete) {
// ✅ shared_from_this()로 수명 연장
auto self = shared_from_this();
std::thread([self, onComplete]() {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
self->doWork(); // self가 살아 있으므로 안전
if (onComplete) onComplete();
}).detach();
}
private:
void doWork() { std::cout << "Async work done\n"; }
};
int main() {
auto worker = std::make_shared<AsyncWorker>();
worker->startAsync( { std::cout << "Callback\n"; });
// main이 바로 반환해도 worker가 스레드에 의해 유지됨
std::this_thread::sleep_for(std::chrono::milliseconds(200));
return 0;
}
실행 결과:
Async work done
Callback
코드 설명:
startAsync에서shared_from_this()로self를 얻어 람다에 캡처합니다.- 람다가
self를 들고 있으므로,main에서worker를 놓아도 스레드가 끝날 때까지 객체가 유지됩니다. this를 직접 캡처하면main반환 후 use-after-free가 발생합니다.
weak_from_this() (C++17)
class MyClass : public std::enable_shared_from_this<MyClass> {
public:
std::weak_ptr<MyClass> getWeak() {
return weak_from_this(); // C++17
}
};
용도: 순환 참조를 피하거나, “있으면 쓰고 없으면 무시”하는 옵저버 패턴에서 사용합니다.
주의사항 요약
| 조건 | 설명 |
|---|---|
| 반드시 shared_ptr로 생성 | make_shared<T>() 또는 shared_ptr<T>(new T) |
| 생성자/소멸자에서 호출 금지 | shared_from_this()는 생성·소멸 완료 후에만 |
| public 상속 | enable_shared_from_this는 반드시 public 상속 |
3. aliasing 생성자
aliasing 생성자란?
aliasing 생성자는 shared_ptr<T>(r, ptr) 형태로, r이 소유권(수명)을 제공하고 ptr이 실제로 가리킬 대상이 됩니다. 즉, “r이 관리하는 객체의 수명에 묶여 있는 ptr”을 가리키는 shared_ptr을 만듭니다.
구문:
template< class Y >
shared_ptr( const shared_ptr<Y>& r, element_type* ptr ) noexcept;
r: 소유권을 공유할 shared_ptr (수명 관리)ptr: 역참조 시 사용할 실제 포인터 (r이 관리하는 객체의 일부여야 함)
부모-멤버 수명 바인딩
flowchart LR
subgraph aliasing["aliasing shared_ptr"]
A1["shared_ptrContainer"] --> A2["제어 블록"]
A2 --> A3["Container 객체"]
A4["shared_ptrBar (aliasing)"] --> A2
A4 -.->|"가리킴"| A5["Bar (멤버)"]
A3 --> A5
end
#include <memory>
#include <iostream>
struct Bar {
int value = 42;
};
struct Container {
Bar member;
};
int main() {
auto container = std::make_shared<Container>();
container->member.value = 100;
// ✅ aliasing: container의 수명에 member가 묶임
std::shared_ptr<Bar> barPtr(container, &container->member);
std::cout << barPtr->value << std::endl; // 100
std::cout << "use_count: " << container.use_count() << std::endl; // 2
container.reset(); // container만 해제해도 barPtr이 아직 유효
std::cout << barPtr->value << std::endl; // 100 (아직 유효)
barPtr.reset(); // 이제 Container와 Bar 모두 해제
return 0;
}
코드 설명:
shared_ptr<Bar>(container, &container->member):container의 제어 블록을 공유하면서, 역참조 시&container->member를 반환합니다.container와barPtr모두 같은 참조 카운트를 공유합니다.container를 reset해도barPtr이 남아 있으면Container객체는 해제되지 않습니다.Bar는Container의 멤버이므로,Container가 삭제되기 전에는Bar에 안전하게 접근할 수 있습니다.
멤버 포인터 반환 함수
struct Container {
Bar member;
std::shared_ptr<Bar> getMember() {
return std::shared_ptr<Bar>(shared_from_this(), &member);
}
};
// 주의: Container가 enable_shared_from_this를 상속해야 함
또는 외부에서 이미 shared_ptr<Container>를 가지고 있다면:
std::shared_ptr<Bar> getBar(std::shared_ptr<Container> c) {
return std::shared_ptr<Bar>(c, &c->member);
}
aliasing과 enable_shared_from_this
주의: aliasing 생성자로 만든 shared_ptr은 enable_shared_from_this를 초기화하지 않습니다. 따라서 Bar가 enable_shared_from_this<Bar>를 상속하고, aliasing으로 만든 shared_ptr<Bar>에서 shared_from_this()를 호출하면 std::bad_weak_ptr이 발생할 수 있습니다. aliasing은 “수명만 공유”하고, 가리키는 객체의 enable_shared_from_this는 설정하지 않습니다.
사용 사례
| 사례 | 설명 |
|---|---|
| 부모의 멤버 반환 | shared_ptr<Parent>로 자식 멤버의 수명 보장 |
| 배열 요소 | shared_ptr<int>(arr, &arr[i])로 i번째 요소 shared_ptr |
| 내부 구현 노출 | Pimpl의 내부 포인터를 수명에 묶어 반환 |
4. 커스텀 삭제자 (Custom Deleter)
커스텀 삭제자란?
shared_ptr은 기본적으로 참조 카운트가 0이 될 때 delete ptr을 호출합니다. FILE*, int[], HANDLE, 소켓 등 delete가 아닌 다른 해제 방식이 필요한 리소스는 커스텀 삭제자를 지정해야 합니다. 삭제자는 void(T* ptr) 시그니처를 만족하는 함수, 람다, std::function 등 호출 가능 객체입니다.
FILE* 관리 (fclose)
#include <memory>
#include <cstdio>
#include <iostream>
int main() {
// ✅ FILE*를 fclose로 해제하는 shared_ptr
std::shared_ptr<FILE> file(
fopen("test.txt", "w"),
{
if (fp) {
std::cout << "fclose 호출\n";
fclose(fp);
}
}
);
if (file) {
fprintf(file.get(), "Hello, shared_ptr!\n");
}
return 0; // 스코프 종료 시 람다가 fclose 호출
}
코드 설명:
shared_ptr<FILE>(fopen(...), lambda): 첫 인자는 포인터, 두 번째는 삭제자입니다.- 람다
{ if (fp) fclose(fp); }가 참조 카운트 0 시 호출됩니다. file.get()으로 raw 포인터를 얻어fprintf에 전달합니다.
배열 관리 (delete[])
#include <memory>
#include <iostream>
int main() {
// ✅ new[]로 할당한 배열 → delete[]로 해제
std::shared_ptr<int> arr(
new int[10],
{
std::cout << "delete[] 호출\n";
delete[] p;
}
);
for (int i = 0; i < 10; ++i) arr.get()[i] = i;
return 0;
}
주의: std::make_shared는 배열을 지원하지 않습니다(C++17 이전). shared_ptr<int>(new int[10], std::default_delete<int[]>()) 또는 C++17의 std::shared_ptr<int[]>를 사용할 수 있습니다.
C++17 default_delete 활용
#include <memory>
// C++17: shared_ptr이 배열을 기본 지원
std::shared_ptr<int[]> arr(new int[100], std::default_delete<int[]>());
// 또는 make_shared 대신 allocate_shared (배열용)
// std::shared_ptr<int[]> arr = std::make_shared<int[]>(100); // C++20
뮤텍스 잠금 해제 (RAII 스타일)
#include <memory>
#include <mutex>
#include <iostream>
void processWithLock(std::mutex& mtx) {
auto lockPtr = std::shared_ptr<std::mutex>(
&mtx,
{
if (m) {
std::cout << "unlock\n";
m->unlock();
}
}
);
mtx.lock();
// ... 작업 ...
// lockPtr 소멸 시 unlock 호출 (주의: mtx 수명이 lockPtr보다 길어야 함)
}
주의: 이 패턴은 mtx가 lockPtr보다 오래 살아 있어야 합니다. 일반적으로 std::unique_lock을 쓰는 것이 더 안전합니다. shared_ptr로 “수명 연장”이 필요할 때만 사용합니다.
삭제자 타입과 제어 블록
커스텀 삭제자를 쓰면 제어 블록에 삭제자가 저장됩니다. 삭제자마다 타입이 다르므로 shared_ptr<T>끼리는 호환되지만, make_shared와 달리 객체와 제어 블록이 분리되어 할당이 2번 발생할 수 있습니다. 성능이 중요하면 make_shared + 기본 delete를 우선 고려하세요.
5. shared_ptr 스레드 안전성
무엇이 스레드 안전한가?
flowchart TB
subgraph safe["✅ 스레드 안전"]
S1["참조 카운트 증감"]
S2["서로 다른 shared_ptr 객체에서 복사/이동"]
S3["객체 소멸 시점 동기화"]
end
subgraph unsafe["❌ 스레드 안전하지 않음"]
U1["같은 shared_ptr 객체 동시 수정"]
U2["get()으로 얻은 raw 포인터 동시 접근"]
end
스레드 안전:
- 참조 카운트의 증감은 원자적(atomic) 연산입니다.
- 서로 다른
shared_ptr객체(복사본)를 여러 스레드가 각각 사용하는 것은 안전합니다. 예: 스레드 A가shared_ptr복사본을 가지고, 스레드 B가 다른 복사본을 가질 때, 각 스레드가 자기 복사본만 수정하면 됩니다.
스레드 안전하지 않음:
- 같은
shared_ptr객체를 여러 스레드가 동시에 수정(reset, 대입 등)하면 데이터 레이스가 발생합니다. get()으로 얻은 raw 포인터를 여러 스레드가 동시에 사용할 때, 그 객체 자체에 대한 동기화는 사용자가 책임져야 합니다.
안전한 패턴: 스레드마다 복사본
#include <memory>
#include <thread>
#include <vector>
#include <iostream>
void worker(std::shared_ptr<int> ptr) {
// 각 스레드는 자기 복사본을 가짐 → 안전
std::cout << "use_count: " << ptr.use_count() << std::endl;
}
int main() {
auto ptr = std::make_shared<int>(42);
std::vector<std::thread> threads;
for (int i = 0; i < 3; ++i) {
threads.emplace_back(worker, ptr); // 값으로 전달 → 복사
}
for (auto& t : threads) t.join();
return 0;
}
코드 설명:
worker에shared_ptr을 값으로 전달하면 각 스레드가 자기 복사본을 받습니다.- 복사 시 참조 카운트만 원자적으로 증가하므로 스레드 안전합니다.
- 각 스레드가 자기 복사본만 사용하므로 데이터 레이스가 없습니다.
위험한 패턴: 같은 shared_ptr 공유 수정
// ❌ 위험: 같은 ptr을 여러 스레드가 수정
std::shared_ptr<int> ptr = std::make_shared<int>(42);
std::thread t1([&ptr]() { ptr = std::make_shared<int>(1); }); // 데이터 레이스!
std::thread t2([&ptr]() { ptr.reset(); }); // 데이터 레이스!
t1.join(); t2.join();
해결: 스레드마다 복사본을 두거나, std::atomic<std::shared_ptr<T>>(C++20)를 사용합니다.
atomic<shared_ptr> (C++20)
여러 스레드가 같은 변수에 shared_ptr을 교체하는 경우, std::atomic<std::shared_ptr<T>>(C++20)를 사용하면 데이터 레이스 없이 동기화할 수 있습니다. load, store, exchange, compare_exchange_strong 등이 모두 지원됩니다.
#include <atomic>
#include <memory>
#include <thread>
#include <iostream>
std::atomic<std::shared_ptr<int>> atomicPtr;
void writer() {
for (int i = 0; i < 5; ++i) {
atomicPtr.store(std::make_shared<int>(i));
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
void reader() {
for (int i = 0; i < 5; ++i) {
auto local = atomicPtr.load(); // 스레드 안전한 읽기
if (local) {
std::cout << "Reader: " << *local << std::endl;
}
std::this_thread::sleep_for(std::chrono::milliseconds(15));
}
}
int main() {
atomicPtr.store(std::make_shared<int>(0));
std::thread t1(writer);
std::thread t2(reader);
t1.join();
t2.join();
return 0;
}
코드 설명:
atomicPtr.store(): 다른 스레드가 안전하게 새 shared_ptr을 쓸 수 있습니다.atomicPtr.load(): 현재 shared_ptr의 복사본을 원자적으로 가져옵니다.- 각 스레드가
local복사본만 사용하므로 데이터 레이스가 없습니다.
compare_exchange로 원자적 교체
#include <atomic>
#include <memory>
std::atomic<std::shared_ptr<int>> atomicPtr;
void updateIfEqual(int expected, int newVal) {
auto current = atomicPtr.load();
while (current && *current == expected) {
auto desired = std::make_shared<int>(newVal);
if (atomicPtr.compare_exchange_strong(current, desired)) {
break; // 성공적으로 교체됨
}
// current가 compare_exchange에서 실패 시 최신 값으로 갱신됨
}
}
참고: C++20 이전에는 std::atomic_load(&ptr), std::atomic_store(&ptr, value) 등을 사용합니다.
// C++17 이전
std::shared_ptr<int> ptr = std::make_shared<int>(42);
std::shared_ptr<int> loaded = std::atomic_load(&ptr);
std::atomic_store(&ptr, std::make_shared<int>(100));
요약 표
| 연산 | 스레드 안전 |
|---|---|
| 복사 생성/대입 (서로 다른 객체) | ✅ |
| 참조 카운트 증감 | ✅ |
| 같은 shared_ptr 객체 동시 수정 | ❌ |
| get() 반환 포인터로의 객체 접근 | 사용자 동기화 필요 |
5. 완전한 예제 코드
예제 1: enable_shared_from_this + 비동기 타이머
#include <memory>
#include <functional>
#include <thread>
#include <chrono>
#include <iostream>
class TimedService : public std::enable_shared_from_this<TimedService> {
public:
void scheduleCallback(int ms, std::function<void()> fn) {
auto self = shared_from_this();
std::thread([self, ms, fn]() {
std::this_thread::sleep_for(std::chrono::milliseconds(ms));
if (fn) fn();
}).detach();
}
};
int main() {
auto svc = std::make_shared<TimedService>();
svc->scheduleCallback(50, { std::cout << "Done\n"; });
std::this_thread::sleep_for(std::chrono::milliseconds(100));
return 0;
}
예제 2: aliasing으로 컨테이너 멤버 반환
#include <memory>
#include <vector>
#include <iostream>
struct Node {
int id;
std::vector<int> data;
};
class NodeManager {
std::shared_ptr<Node> root_;
public:
NodeManager() : root_(std::make_shared<Node>()) {
root_->id = 1;
root_->data = {1, 2, 3};
}
std::shared_ptr<std::vector<int>> getData() {
return std::shared_ptr<std::vector<int>>(root_, &root_->data);
}
};
int main() {
NodeManager mgr;
auto dataPtr = mgr.getData();
for (int x : *dataPtr) std::cout << x << " ";
std::cout << std::endl;
return 0;
}
예제 3: 스레드 풀에 shared_ptr 작업 전달
#include <memory>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <iostream>
class ThreadPool {
std::vector<std::thread> workers_;
std::queue<std::function<void()>> tasks_;
std::mutex mtx_;
std::condition_variable cv_;
bool stop_ = false;
public:
ThreadPool(size_t n) {
for (size_t i = 0; i < n; ++i) {
workers_.emplace_back([this]() {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lk(mtx_);
cv_.wait(lk, [this] { return stop_ || !tasks_.empty(); });
if (stop_ && tasks_.empty()) return;
task = std::move(tasks_.front());
tasks_.pop();
}
task();
}
});
}
}
template<typename F>
void submit(F&& f) {
{
std::lock_guard<std::mutex> lk(mtx_);
tasks_.emplace(std::forward<F>(f));
}
cv_.notify_one();
}
~ThreadPool() {
{ std::lock_guard<std::mutex> lk(mtx_); stop_ = true; }
cv_.notify_all();
for (auto& w : workers_) w.join();
}
};
int main() {
ThreadPool pool(2);
auto config = std::make_shared<int>(42);
pool.submit([config]() {
std::cout << "Task: " << *config << std::endl;
});
std::this_thread::sleep_for(std::chrono::milliseconds(100));
return 0;
}
예제 4: enable_shared_from_this + aliasing 조합
#include <memory>
#include <iostream>
struct Payload { int value = 0; };
class Handler : public std::enable_shared_from_this<Handler> {
Payload payload_;
public:
Handler(int v) { payload_.value = v; }
std::shared_ptr<Payload> getPayload() {
return std::shared_ptr<Payload>(shared_from_this(), &payload_);
}
};
int main() {
auto handler = std::make_shared<Handler>(100);
auto payload = handler->getPayload();
std::cout << payload->value << std::endl; // 100
handler.reset();
std::cout << payload->value << std::endl; // 100 (payload가 수명 유지)
return 0;
}
예제 5: 커스텀 삭제자 (FILE*, 배열)
#include <memory>
#include <cstdio>
#include <iostream>
int main() {
// FILE*: fclose로 해제
auto file = std::shared_ptr<FILE>(
fopen("log.txt", "w"),
{ if (fp) fclose(fp); }
);
if (file) fprintf(file.get(), "Log entry\n");
// 배열: delete[]로 해제
auto arr = std::shared_ptr<int>(
new int[5]{1, 2, 3, 4, 5},
{ delete[] p; }
);
for (int i = 0; i < 5; ++i) std::cout << arr.get()[i] << " ";
std::cout << std::endl;
return 0;
}
예제 6: weak_ptr 옵저버 패턴
#include <memory>
#include <vector>
#include <functional>
#include <iostream>
class Observer;
class Subject : public std::enable_shared_from_this<Subject> {
std::vector<std::weak_ptr<Observer>> observers_;
public:
void attach(std::shared_ptr<Observer> obs) {
observers_.push_back(obs); // weak_ptr로 저장 (참조 카운트 증가 안 함)
}
void notify() {
for (auto& wp : observers_) {
auto sp = wp.lock();
if (sp) {
sp->onNotify();
}
// expired면 무시 (이미 삭제된 구독자)
}
}
};
class Observer : public std::enable_shared_from_this<Observer> {
int id_;
public:
Observer(int id) : id_(id) {}
void onNotify() {
std::cout << "Observer " << id_ << " notified\n";
}
};
int main() {
auto subject = std::make_shared<Subject>();
auto obs1 = std::make_shared<Observer>(1);
auto obs2 = std::make_shared<Observer>(2);
subject->attach(obs1);
subject->attach(obs2);
subject->notify(); // Observer 1, 2 notified
obs1.reset(); // obs1 해제
subject->notify(); // Observer 2만 notified (obs1은 expired)
return 0;
}
코드 설명:
Subject는vector<weak_ptr<Observer>>로 구독자를 저장합니다. shared_ptr이 아니므로 참조 카운트를 올리지 않아 구독자가 스스로 해제될 수 있습니다.notify()에서lock()으로shared_ptr을 얻고, 유효하면 콜백을 호출합니다.expired()인 구독자는 건너뜁니다.- 이 패턴으로 “있으면 알리고, 없으면 무시”하는 옵저버 패턴을 구현할 수 있습니다.
7. 자주 발생하는 에러와 해결법
에러 1: shared_ptr(this)로 이중 해제
증상: 프로그램 크래시 (double-free 또는 corruption)
// ❌ 잘못된 코드
class Bad {
public:
std::shared_ptr<Bad> getPtr() {
return std::shared_ptr<Bad>(this);
}
};
auto p1 = std::make_shared<Bad>();
auto p2 = p1->getPtr(); // 서로 다른 제어 블록 → 이중 해제
해결법:
// ✅ 올바른 코드
class Good : public std::enable_shared_from_this<Good> {
public:
std::shared_ptr<Good> getPtr() {
return shared_from_this();
}
};
에러 2: shared_from_this()를 스택 객체에서 호출
증상: std::bad_weak_ptr 예외
// ❌ 잘못된 코드
class Service : public std::enable_shared_from_this<Service> {};
Service svc; // 스택에 생성
auto p = svc.shared_from_this(); // bad_weak_ptr!
해결법:
// ✅ 올바른 코드: 반드시 shared_ptr로 생성
auto svc = std::make_shared<Service>();
auto p = svc->shared_from_this();
에러 3: 생성자에서 shared_from_this() 호출
증상: std::bad_weak_ptr 예외
// ❌ 잘못된 코드
class Bad : public std::enable_shared_from_this<Bad> {
public:
Bad() {
auto p = shared_from_this(); // 아직 shared_ptr이 없음!
}
};
해결법:
// ✅ 올바른 코드: 생성자 밖에서 호출
class Good : public std::enable_shared_from_this<Good> {
public:
void init() {
auto p = shared_from_this(); // init()은 생성 후 호출
}
};
auto obj = std::make_shared<Good>();
obj->init();
에러 4: 멤버 포인터를 독립 shared_ptr로 생성
증상: use-after-free (부모가 먼저 삭제됨)
// ❌ 잘못된 코드
auto container = std::make_shared<Container>();
std::shared_ptr<Bar> barPtr(container->member); // 잘못된 사용
// 또는
std::shared_ptr<Bar> barPtr(&container->member); // container와 무관한 제어 블록!
해결법:
// ✅ 올바른 코드: aliasing 생성자
std::shared_ptr<Bar> barPtr(container, &container->member);
에러 5: 같은 shared_ptr을 여러 스레드가 동시 수정
증상: 데이터 레이스, 정의되지 않은 동작
// ❌ 잘못된 코드
std::shared_ptr<int> ptr = std::make_shared<int>(42);
std::thread t1([&ptr]() { ptr.reset(); });
std::thread t2([&ptr]() { ptr = std::make_shared<int>(1); });
해결법:
// ✅ 올바른 코드: 스레드마다 복사본
std::shared_ptr<int> ptr = std::make_shared<int>(42);
std::thread t1([ptr]() { /* ptr 복사본 사용 */ });
std::thread t2([ptr]() { /* ptr 복사본 사용 */ });
에러 6: 비동기 콜백에 this 직접 캡처
증상: use-after-free
// ❌ 잘못된 코드
void startAsync() {
asyncCall([this]() {
this->onComplete(); // this가 이미 삭제됐을 수 있음!
});
}
해결법:
// ✅ 올바른 코드
void startAsync() {
auto self = shared_from_this();
asyncCall([self]() {
self->onComplete();
});
}
에러 7: 배열에 delete 사용 (delete[] 누락)
증상: 미정의 동작, 힙 손상
// ❌ 잘못된 코드
std::shared_ptr<int> arr(new int[10]); // 기본 delete 사용 → delete[] 아님!
해결법:
// ✅ 올바른 코드: 커스텀 삭제자로 delete[]
std::shared_ptr<int> arr(new int[10], { delete[] p; });
// 또는 C++17
std::shared_ptr<int[]> arr(new int[10], std::default_delete<int[]>());
에러 8: weak_ptr lock() 결과 검사 누락
증상: null 역참조, use-after-free
// ❌ 잘못된 코드
void callback(std::weak_ptr<Service> wp) {
wp.lock()->doWork(); // lock()이 nullptr 반환하면 크래시!
}
해결법:
// ✅ 올바른 코드: lock() 결과 검사
void callback(std::weak_ptr<Service> wp) {
auto sp = wp.lock();
if (sp) {
sp->doWork();
}
}
에러 9: 커스텀 삭제자에서 예외 발생
증상: 참조 카운트 0 시 삭제자가 호출되는데, 삭제자에서 예외를 던지면 std::terminate 호출
// ❌ 위험: 삭제자에서 예외
std::shared_ptr<Resource> r(ptr, {
if (!p->close()) throw std::runtime_error("close failed"); // terminate!
});
해결법:
// ✅ 올바른 코드: 삭제자에서는 예외를 던지지 않음
std::shared_ptr<Resource> r(ptr, {
if (p) {
try {
p->close();
} catch (...) {
std::cerr << "close failed\n"; // 로깅만, 예외 전파 금지
}
}
});
8. 모범 사례
선택 플로우차트
flowchart TD
A["this를 shared_ptr로 넘겨야 하나?"] -->|Yes| B["enable_shared_from_this 상속"]
A -->|No| C["멤버 포인터 수명을 부모에 묶어야 하나?"]
C -->|Yes| D["aliasing 생성자 사용"]
C -->|No| E["일반 shared_ptr 사용"]
B --> F["shared_from_this() 사용"]
F --> G["반드시 make_shared로 생성"]
모범 사례 요약
| 규칙 | 설명 |
|---|---|
| this → shared_ptr 필요 시 | enable_shared_from_this + shared_from_this() |
| shared_from_this 사용 클래스 | 반드시 make_shared로 생성 |
| 생성자/소멸자 | shared_from_this() 호출 금지 |
| 부모 수명에 묶인 멤버 | aliasing 생성자 shared_ptr(r, ptr) |
| 멀티스레드 | 스레드마다 shared_ptr 복사본, 또는 atomic |
| 비동기 콜백 | this 대신 shared_from_this() 캡처 |
| FILE*/배열 등 | 커스텀 삭제자 지정 |
| 옵저버 패턴 | 발행자는 weak_ptr로 구독자 저장 |
| 삭제자 | 예외 던지지 않기 |
| weak_ptr 사용 시 | lock() 결과 반드시 검사 |
enable_shared_from_this 체크리스트
- 클래스가
enable_shared_from_this를 public 상속하는가? - 객체를 항상
make_shared또는shared_ptr(new T)로 생성하는가? -
shared_from_this()를 생성자/소멸자에서 호출하지 않는가? - 비동기 콜백에
this대신shared_from_this()를 넘기는가?
aliasing 체크리스트
-
ptr이r이 관리하는 객체의 일부(멤버, 배열 요소 등)인가? -
ptr의 수명이r에 묶여 있어야 하는 상황인가? - aliasing으로 만든 shared_ptr에서
shared_from_this()를 호출하지 않는가? (해당 객체가 enable_shared_from_this를 상속한 경우)
커스텀 삭제자 체크리스트
-
delete가 아닌 해제 방식이 필요한가? (FILE*, 배열, HANDLE 등) - 삭제자에서 예외를 던지지 않는가?
- 배열의 경우
delete[]또는default_delete<T[]>를 사용하는가?
weak_ptr 옵저버 체크리스트
- 발행자가 구독자 수명을 소유하지 않아도 되는가?
-
lock()호출 후 반드시 null 검사를 하는가? -
expired()인 구독자는 안전하게 건너뛰는가?
9. 프로덕션 패턴
패턴 1: 네트워크 세션 (enable_shared_from_this)
class Session : public std::enable_shared_from_this<Session> {
asio::ip::tcp::socket socket_;
public:
void asyncRead() {
auto self = shared_from_this();
socket_.async_read_some(buffer_, [self](error_code ec, size_t len) {
if (!ec) self->onRead(len);
});
}
};
// Session은 make_shared로 생성
패턴 2: 이벤트 구독자 (shared_from_this)
class Subscriber : public std::enable_shared_from_this<Subscriber> {
public:
void subscribe(EventBus& bus) {
bus.add(shared_from_this());
}
};
패턴 3: 캐시에서 멤버 반환 (aliasing)
std::shared_ptr<Config> Cache::getConfig(const std::string& key) {
auto entry = getEntry(key); // shared_ptr<CacheEntry>
return std::shared_ptr<Config>(entry, &entry->config_);
}
패턴 4: 스레드 풀 작업 (shared_ptr 캡처)
void submitTask(std::shared_ptr<Context> ctx) {
pool.submit([ctx]() {
process(ctx); // ctx 수명이 작업 완료까지 유지
});
}
패턴 5: 팩토리 + enable_shared_from_this
class Service : public std::enable_shared_from_this<Service> {
Service() = default; // private
public:
static std::shared_ptr<Service> create() {
return std::shared_ptr<Service>(new Service());
}
};
패턴 6: 파일 핸들 RAII (커스텀 삭제자)
using FileHandle = std::shared_ptr<FILE>;
FileHandle openFile(const char* path, const char* mode) {
FILE* fp = fopen(path, mode);
if (!fp) return nullptr;
return FileHandle(fp, { if (f) fclose(f); });
}
void processFile(FileHandle f) {
if (f) fprintf(f.get(), "data\n");
} // 여러 함수가 FileHandle을 공유해도, 마지막 소유자가 없어지면 fclose
패턴 7: weak_ptr 옵저버 (이벤트 버스)
class EventBus {
std::vector<std::weak_ptr<EventListener>> listeners_;
public:
void subscribe(std::shared_ptr<EventListener> l) {
listeners_.push_back(l);
}
void publish(const Event& e) {
for (auto it = listeners_.begin(); it != listeners_.end(); ) {
auto sp = it->lock();
if (sp) {
sp->onEvent(e);
++it;
} else {
it = listeners_.erase(it); // 만료된 구독자 제거
}
}
}
};
패턴 8: atomic<shared_ptr>로 동적 설정 교체
std::atomic<std::shared_ptr<Config>> globalConfig;
void reloadConfig() {
auto newConfig = std::make_shared<Config>(loadFromFile());
globalConfig.store(newConfig); // 원자적 교체
}
void worker() {
auto config = globalConfig.load(); // 항상 최신 또는 이전 버전 (안전)
if (config) config->apply();
}
10. 성능과 체크리스트
enable_shared_from_this 오버헤드
enable_shared_from_this는 내부에weak_ptr을 하나 추가합니다.- 객체 크기가 약간 증가하고,
make_shared시 제어 블록에 weak_ptr 관련 공간이 필요합니다. shared_from_this()호출 시 weak_ptr을 shared_ptr로 변환하는 비용이 있으나, 일반적으로 무시할 수준입니다.
aliasing 생성자 오버헤드
- aliasing 생성자는 새 제어 블록을 만들지 않습니다. 기존 shared_ptr의 제어 블록을 공유합니다.
- 참조 카운트만 증가하므로 오버헤드가 적습니다.
프로덕션 체크리스트
-
enable_shared_from_this사용 시make_shared로 생성 - 생성자/소멸자에서
shared_from_this()호출 금지 - 멤버 포인터 반환 시 aliasing 생성자 사용
- 멀티스레드에서 shared_ptr 공유 시 스레드마다 복사본 또는 atomic
- 비동기 콜백에
this대신shared_from_this()전달
마무리
핵심 요약
✅ enable_shared_from_this
this를 안전하게 shared_ptr로 변환shared_ptr(this)는 이중 해제 유발 → 사용 금지- 반드시
make_shared로 생성, 생성자/소멸자에서 호출 금지
✅ aliasing 생성자
shared_ptr<T>(r, ptr): r의 수명에 ptr을 묶음- 부모 멤버를 shared_ptr로 반환할 때 사용
✅ 커스텀 삭제자
- FILE*, 배열, HANDLE 등 delete가 아닌 리소스용
shared_ptr<T>(ptr, deleter)형태, 삭제자에서 예외 금지
✅ atomic<shared_ptr> (C++20)
- 같은 변수에 여러 스레드가 shared_ptr 교체 시 사용
load,store,compare_exchange_strong지원
✅ weak_ptr 옵저버 패턴
- 발행자는
weak_ptr로 구독자 저장,lock()으로 접근 lock()결과 반드시 검사
✅ 스레드 안전성
- 참조 카운트는 원자적
- 같은 shared_ptr 객체 동시 수정은 데이터 레이스
- 스레드마다 복사본 보유 권장
다음 글
weak_ptr과 순환 참조 해결은 C++ 스마트 포인터와 순환 참조 해결법을 참고하세요.
자주 묻는 질문 (FAQ)
Q. shared_from_this()를 생성자에서 호출할 수 없나요?
A. 불가능합니다. 생성자가 실행되는 시점에는 아직 shared_ptr이 객체를 소유하지 않아, 내부 weak_ptr이 초기화되지 않습니다. 초기화 로직이 필요하면 void init() 같은 메서드를 두고, make_shared로 생성한 뒤 init()을 호출하세요.
Q. aliasing으로 만든 shared_ptr에서 shared_from_this()를 쓸 수 있나요?
A. 가리키는 객체(ptr)가 enable_shared_from_this를 상속했고, 그 객체가 shared_ptr로 직접 생성된 경우에만 가능합니다. aliasing 생성자는 enable_shared_from_this를 초기화하지 않으므로, aliasing으로만 만들어진 shared_ptr에서는 shared_from_this()가 실패할 수 있습니다.
Q. shared_ptr을 함수 인자로 넘길 때 값으로 받나요, const 참조로 받나요?
A. 함수가 소유권을 공유해야 하면 값으로 받는 것이 좋습니다. 복사가 발생하지만 참조 카운트만 원자적으로 증가하며, 호출자가 std::move로 넘길 수도 있습니다. 읽기만 하고 소유권을 늘리지 않을 때는 const shared_ptr<T>&를 사용합니다.
Q. 커스텀 삭제자를 쓸 때 make_shared를 사용할 수 있나요?
A. make_shared는 커스텀 삭제자를 지원하지 않습니다. shared_ptr<T>(new T(...), deleter) 형태로 생성해야 합니다. 이 경우 객체와 제어 블록이 분리되어 할당이 2번 발생할 수 있어, make_shared보다 약간의 오버헤드가 있습니다.
Q. 옵저버 패턴에서 shared_ptr 대신 weak_ptr을 쓰는 이유는?
A. 발행자가 shared_ptr로 구독자를 들면, 구독자가 자기 자신을 해제해도 발행자가 소유하고 있어 객체가 영원히 살아 있습니다. weak_ptr은 참조 카운트를 올리지 않으므로, 구독자가 스스로 해제될 수 있고, 발행자는 lock()으로 “있으면 쓰고, 없으면 무시”할 수 있습니다.
참고 자료
- C++ Reference - shared_ptr
- C++ Reference - enable_shared_from_this
- C++ Core Guidelines - Smart Pointers
- Effective Modern C++ - Item 19-22 (Scott Meyers)
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 스마트 포인터 기초 완벽 가이드 | unique_ptr·shared_ptr
- C++ 스마트 포인터와 순환 참조(Circular Reference) 해결법 [#33-3]
- C++ RAII | “파일을 열 수 없습니다” 장애의 원인과 자동 리소스 관리
이 글에서 다루는 키워드 (관련 검색어)
C++, shared_ptr, enable_shared_from_this, aliasing, weak_ptr, custom_deleter, atomic_shared_ptr, 스레드안전성, 스마트포인터, 메모리관리 등으로 검색하시면 이 글이 도움이 됩니다.
관련 글
- C++ 스마트 포인터 기초 완벽 가이드 | unique_ptr·shared_ptr
- C++ Google Test | gtest 설치부터 TEST·EXPECT_EQ
- C++ Google Mock |
- C++ unique_ptr 고급 완벽 가이드 | 커스텀 삭제자·배열
- C++ CMake 고급 | 멀티 타겟·외부 라이브러리 관리 (대규모 프로젝트 빌드)