본문으로 건너뛰기
Embedded C++ for Real Systems · 27/41

임베디드 스마트 포인터 선택 — unique·shared·custom 비교

· Hawk · 5분 읽기

#한 줄 요약

“임베디드에서는 unique_ptr이 기본입니다.” shared_ptr은 거의 피하고, raw pointer는 non-owning 참조에만 씁니다.

#어떤 문제를 푸는가

C++의 스마트 포인터 세트는 다음과 같습니다.

  • **unique_ptr<T>**는 단일 소유를 표현하며 RAII로 자동 해제합니다.
  • **shared_ptr<T>**는 공유 소유를 표현하며 atomic 카운터를 사용합니다.
  • **weak_ptr<T>**는 shared의 non-owning 참조입니다.
  • **raw pointer(T*)**는 non-owning이며 소유권을 갖지 않습니다.

각각을 임베디드에서 언제 쓸지 정리합니다.

#unique_ptr<T> — 단일 소유

#include <memory>
std::unique_ptr<Sensor> sensor = std::make_unique<Sensor>();
// sensor가 유일한 owner
// 소멸 시 자동 delete
// 이동 가능
auto other = std::move(sensor); // sensor → other로 소유권 이전
// sensor는 이제 nullptr

크기는 deleter가 비어 있다면 정확히 sizeof(T*)입니다. 추가 오버헤드가 0입니다.

#임베디드 활용

class Driver {
public:
static std::unique_ptr<Driver> create(int param) {
if (!validate(param)) return nullptr;
return std::make_unique<Driver>(param);
}
};
void setup() {
auto driver = Driver::create(42);
if (driver) {
driver->initialize();
// ... 사용
// 함수 끝 — 자동 delete
}
}

RAII와 명시적 소유권을 동시에 제공하므로 가장 권장합니다.

#Custom deleter — pool 활용

struct PoolDeleter {
Pool* pool;
void operator()(uint8_t* p) const {
if (p) pool->deallocate(p);
}
};
using PoolPtr = std::unique_ptr<uint8_t, PoolDeleter>;
PoolPtr alloc(Pool& p, size_t n) {
return PoolPtr(p.allocate(n), {&p});
}

heap을 전혀 쓰지 않고 pool에서 할당한 뒤 자동으로 해제합니다.

#shared_ptr<T> — 공유 소유

auto a = std::make_shared<Resource>();
auto b = a; // copy — 카운터 증가 (atomic)
// a와 b 모두 owner — 마지막이 사라질 때 delete

크기는 T pointer와 control block pointer를 합쳐 8 byte입니다(32-bit ARM에서도 8 byte).

내부의 control block은 별도로 할당되며 다음을 포함합니다.

  • strong과 weak 두 개의 atomic 카운터
  • deleter
  • allocator
  • 보통 16~32 byte의 heap

#임베디드에서의 문제

  1. atomic 카운터가 multi-core RTOS에서 cache coherence 오버헤드를 만듭니다.
  2. control block이 heap을 사용합니다(make_shared는 1번, 별도 new는 2번 할당).
  3. cyclic reference에서 weak_ptr을 쓰지 않으면 leak이 발생합니다.

대부분의 임베디드 환경에서는 shared_ptr을 회피합니다. 사용하려면 명확한 정당화가 필요합니다.

#shared_ptr이 정말 필요한 경우

  • 여러 owner가 진짜로 필요해서 한 명의 owner로 표현이 어려운 경우
  • RTOS 다중 task에서 동시 접근이 필요한 경우
  • 비동기 callback에서 객체 lifetime을 보장해야 하는 경우
class Job {
std::shared_ptr<Data> data_;
public:
Job(std::shared_ptr<Data> d) : data_(std::move(d)) {}
void execute() {
// data_가 다른 task에서도 사용 중
process(*data_);
}
};

#weak_ptr<T> — non-owning 관찰

auto strong = std::make_shared<Resource>();
std::weak_ptr<Resource> weak = strong;
// 사용
if (auto locked = weak.lock()) {
// strong 유효 — locked가 잠시 owner
} else {
// strong 이미 해제됨
}

cyclic reference를 회피할 때 씁니다.

struct Parent {
std::vector<std::shared_ptr<Child>> children;
};
struct Child {
std::weak_ptr<Parent> parent; // shared 아님 — cycle 방지
};

임베디드에서 weak_ptr을 쓰려면 shared_ptr이 전제되어야 하므로 둘 다 권장하지 않습니다.

#Raw pointer — non-owning

class Cache {
public:
void register_handler(Handler* h) { // non-owning
handler_ = h;
}
void on_event() {
if (handler_) handler_->handle();
}
private:
Handler* handler_ = nullptr;
};
Handler h;
cache.register_handler(&h); // 소유권 이전 X — h는 호출자가 관리

raw pointer는 non-owning reference 역할만 하며, 소유자는 따로 둡니다.

C++ Core Guidelines의 권장은 다음과 같습니다.

  • Owning pointer는 unique_ptr이나 shared_ptr로 표현합니다.
  • Non-owning pointer는 raw T*나 reference T&로 표현합니다.

C++ Core Guidelines의 gsl::owner<T*> 마커로 의도를 명시할 수도 있습니다.

#결정 트리

스마트 포인터 결정 트리 — 소유권 기준으로 unique_ptr, shared_ptr, raw pointer 선택

임베디드에서는 대부분 unique_ptr 또는 raw 참조로 정리됩니다.

#임베디드 패턴 — Factory + unique_ptr

class Device {
Device() = default; // private
public:
static std::unique_ptr<Device> create(const Config& cfg) {
if (!validate(cfg)) return nullptr;
auto d = std::unique_ptr<Device>(new Device);
if (!d->initialize(cfg)) return nullptr;
return d;
}
};
// 사용
auto device = Device::create(my_config);
if (!device) {
log_error("device create failed");
return;
}
device->run();
// 자동 cleanup

생성 실패 처리, RAII, 명시 소유를 한 번에 해결합니다.

#임베디드 패턴 — Optional 멤버 with unique_ptr

class System {
std::unique_ptr<Logger> logger_; // nullable
public:
void enable_logging() {
logger_ = std::make_unique<Logger>();
}
void log(const char* msg) {
if (logger_) logger_->write(msg);
}
};

logger가 있을 수도 없을 수도 있는 상황에서 동적으로 활성화합니다.

대안은 heap을 쓰지 않는 std::optional<Logger>입니다.

class System {
std::optional<Logger> logger_; // 더 좋음 (heap 없음)
public:
void enable_logging() {
logger_.emplace();
}
};

logger_가 내부에 Logger 객체를 직접 저장하므로 heap을 쓰지 않습니다. 임베디드에서 선호합니다.

#자주 보는 함정과 안티패턴

#1. new 직접 사용

auto* p = new Foo(); // raw owning pointer — 어떻게 delete?

항상 make_unique를 씁니다. raw owning은 금지합니다.

#2. shared_ptr 무심코 사용

shared_ptr<Data> get_data(); // 정말 공유 소유?

unique_ptr로 충분하면 unique로 갑니다. shared는 정당화가 필요합니다.

#3. unique_ptr의 함수 포인터 deleter

std::unique_ptr<FILE, decltype(&fclose)> f(fopen(...), &fclose);

sizeof가 두 배가 됩니다. struct나 lambda deleter를 씁니다.

#4. weak_ptr 없이 cycle

struct A { shared_ptr<B> b; };
struct B { shared_ptr<A> a; }; // cycle — 영원히 안 해제

한쪽을 weak_ptr로 둡니다.

#5. unique_ptr copy 시도

auto a = std::make_unique<Foo>();
auto b = a; // ERROR — copy 안 됨

move를 씁니다(auto b = std::move(a);).

#6. shared_ptr atomic overhead 무시

multi-core에서는 카피마다 atomic 연산이 일어나므로 hot path에 부담이 됩니다. raw pointer와 명확한 owner로 대체합니다.

#측정 — 스마트 포인터 비교

같은 객체를 1만 회 생성하고 해제합니다.

방식alloc cyclestotal (1만회)비고
new/delete (raw)~1201.2 ms
unique_ptr~1201.2 msmake_unique + new
shared_ptr~2002.0 mscontrol block 추가, copy +10 cycles
unique_ptr + pool~150.15 mspool 할당

pool과 unique_ptr 조합이 10배 빠릅니다. shared_ptr은 두 배 가까이 느립니다.

#std::shared_ptr 대안 — Reference counting을 직접

class RefCounted {
mutable int count_ = 1;
public:
void add_ref() const { ++count_; }
void release() const {
if (--count_ == 0) delete this;
}
protected:
virtual ~RefCounted() = default;
};
template<typename T>
class IntrusivePtr {
T* p_;
public:
explicit IntrusivePtr(T* p) : p_(p) { if (p_) p_->add_ref(); }
~IntrusivePtr() { if (p_) p_->release(); }
// ...
};

Boost.IntrusivePtr와 같은 아이디어입니다. 별도의 control block이 없고 메모리 추가도 적습니다. multi-thread 환경에서는 count_를 atomic으로 둡니다.

#정리

  • 임베디드에서는 unique_ptr이 기본입니다. 명시적 소유와 RAII를 zero overhead로 제공합니다.
  • shared_ptr은 회피합니다. atomic 카운터와 heap 위의 control block이 부담입니다.
  • raw pointer는 non-owning에만 쓰고 소유권은 항상 unique나 shared로 표현합니다.
  • Custom deleter(struct나 lambda)로 pool, FD, 비-heap 자원을 관리합니다.
  • Optional 멤버는 std::optional<T>(heap 0)를 우선합니다.

#관련 항목

#다음 글

Part 3-10: 소유권 모델 — 객체 lifetime에 책임을 명확히 할당하는 owner, observer, borrower 모델을 다룹니다.

Embedded C++ for Real Systems · 28 of 41

  1. 1Embedded C++ for Real Systems — 임베디드 모던 C++ 시리즈 소개
  2. 2임베디드 C++ vs C — 런타임·코드 크기·ABI 관점 비교
  3. 3임베디드 C++ 컴파일러 플래그 분석 — -fno-rtti·-fno-exceptions·-Os
  4. 4임베디드 C++ 런타임 요구사항 — libstdc++·newlib·crt0 분석
  5. 5C++ 코드 크기 분석 — 가상 함수·템플릿·예외 비용 추적
  6. 6C++ ABI 호환성 — Itanium ABI·name mangling·vtable 레이아웃
  7. 7C++ 스타트업 코드 분석 — .init_array·전역 생성자 호출 순서
  8. 8임베디드 C++ 링커 스크립트 — vtable·정적 객체 배치 추적
  9. 9임베디드 C++ 표준 선택 가이드 — C++11/14/17/20/23 트레이드오프
  10. 10임베디드 RAII 기초 — 리소스 안전성과 결정적 소멸 보장
  11. 11임베디드 RAII 실전 패턴 — Lock·Pin·DMA·Power 관리
  12. 12constexpr 기초와 임베디드 적용 — 컴파일 타임 계산 활용
  13. 13constexpr 고급 활용 — 룩업 테이블·CRC·해시 컴파일 타임 생성
  14. 14consteval과 constinit 분석 — C++20 컴파일 타임 강제 메커니즘
  15. 15임베디드 Templates 기초 — 타입 안전과 코드 재사용 분석
  16. 16Template 비용 분석 — 코드 폭증·인스턴스화·디버그 정보 측정
  17. 17CRTP 패턴 분석 — vtable 없는 정적 다형성
  18. 18Type Traits 임베디드 활용 — SFINAE·is_pod·컴파일 타임 검사
  19. 19C++20 Concepts 활용 — 템플릿 제약과 가독성 개선
  20. 20동적 할당 없는 임베디드 C++ — placement new·정적 객체·풀
  21. 21Custom Allocator 기초 — std::allocator 인터페이스 분석
  22. 22Pool Allocator 구현 — Fixed-Size Block과 O(1) 보장
  23. 23std::pmr 임베디드 활용 — Polymorphic Memory Resource 분석
  24. 24No-Exception C++ 설계 — 코드 크기·결정성 트레이드오프
  25. 25임베디드 에러 처리 패턴 — Result·errno·optional 비교
  26. 26std::expected 분석 — C++23 결과 타입과 에러 전파
  27. 27No-RTTI C++ 설계 — dynamic_cast 제거와 정적 타입 분기
  28. 28임베디드 스마트 포인터 선택 — unique·shared·custom 비교
  29. 29임베디드 C++ 소유권 모델 — single·shared·borrow 패턴
  30. 30Intrusive Containers 분석 — 동적 할당 없는 컨테이너 설계
  31. 31ETL 라이브러리 분석 — Embedded Template Library의 STL 대체
  32. 32임베디드 Lock-free 기초 — atomic·memory ordering·CAS
  33. 33Lock-free Container 구현 — SPSC Queue·Ring Buffer
  34. 34Type-safe Flags 패턴 — Enum Class·Strong Typedef·Tag
  35. 35임베디드 State Machine 패턴 — Variant·Visitor·Table-driven 비교
  36. 36Compile-time FSM 구현 — 템플릿으로 상태 전이 검증
  37. 37Singleton 대안 패턴 — Service Locator·Static Init·Phantom
  38. 38MMIO Register 추상화 — 타입 안전한 비트 필드 접근
  39. 39GPIO 추상화 패턴 — Template·Concept으로 보드 독립성
  40. 40Peripheral 추상화 — UART·SPI·I2C 공통 인터페이스 설계
  41. 41임베디드 HAL 설계 패턴 — Static·Dynamic·Hybrid 비교