Type Erasure
TL;DR
- std::function과 std::shared_ptr는 모두 Type Erasure 기반으로 구현된다.
- Type Erasure = 서로 관계없는 다양한 타입을 단일 인터페이스로 감싸 공통적으로 다루는 기법.
- std::function은 호출 인터페이스(call operator)를 숨기기 위해 Concept–Model 구조를 사용한다.
- std::shared_ptr는 deleter(소멸자 호출 방식)를 숨기기 위해 같은 패턴을 사용한다.
- 공통점:
- “원래 타입 정보”를 내부에 저장해 런타임에 올바른 동작(호출/소멸)을 보장한다.
- 내부적으로 virtual dispatch + heap allocation이 발생한다.
Overview
1) 왜 std::function이 가능한가?
std::function<void(int)>이라는 단일 타입에- free function
- lambda
- functor
완전히 종류가 다른 객체를 모두 대입할 수 있다.
- C++은 강한 정적 타입 언어인데 이런 일이 가능한 이유는 Type Erasure 덕분.
2) Type Erasure 핵심 개념
| 구성 | 설명 |
|---|---|
| Concept(인터페이스) | 호출해야 하는 기능만 정의된 순수 가상 클래스 |
| Model |
T 타입을 감싸 Concept을 구현한 래퍼 |
| Container | Concept*를 멤버로 갖고, 호출 시 virtual dispatch로 Model |
즉,
여러 타입(T) → Model<T>로 감싸기 → _Concept 형태로 저장_* → 공통 인터페이스처럼 사용
이 흐름이 std::function의 정체다.
3) std::shared_ptr가 동일 기법을 쓰는 이유
std::shared_ptr<void>는 가능한데std::unique_ptr<void>는 기본 deleter 때문에 삭제가 불가능하다.
왜?
unique_ptr<void>→ deleter가default_delete<void>라서 _void delete가 불가능_*shared_ptr<void>→ 객체를 생성할 때 원래 객체 타입 T를 control block에 보관 → 삭제할 때 T의 destructor 호출 OK
즉, shared_ptr 내부에도
- control block + virtual deleter 구조의 Type Erasure 존재.
4) std::function의 비용
- virtual 호출 비용
- heap allocation 비용
- 단, 구현체에 따라 SBO(Small Buffer Optimization)로 stack 저장될 수도 있음
Example
1) std::function Type Erasure 구조 (축약 버전)
struct Concept {
virtual void call(int) = 0;
virtual ~Concept() = default;
};
template<typename T>
struct Model : Concept {
T obj;
Model(T o) : obj(o) {}
void call(int x) override { obj(x); }
};
class Function {
std::unique_ptr<Concept> ptr;
public:
template<typename F>
Function(F f) : ptr(std::make_unique<Model<F>>(f)) {}
void operator()(int x) { ptr->call(x); }
};
이렇게 하면 다음이 모두 작동한다:
Function f1 = printNum;
Function f2 = [](int x){ std::cout << x; };
Function f3 = PrintNumFunctor{};
2) shared_ptr Type Erasure 구조
struct ControlBlockBase {
size_t refcount = 1;
virtual void destroy() = 0;
virtual ~ControlBlockBase() = default;
};
template<typename T>
struct ControlBlock : ControlBlockBase {
T* ptr;
ControlBlock(T* p) : ptr(p) {}
void destroy() override { delete ptr; }
};
shared_ptr는 다음을 저장한다:
T* obj_ptrControlBlockBase* ctrl
template<typename T>
class SharedPtr {
T* ptr;
ControlBlockBase* ctrl;
public:
template<typename U>
SharedPtr(U* p)
: ptr(p),
ctrl(new ControlBlock<U>(p)) {}
};
즉, U 타입을 보관하는 ControlBlock<U> 덕에
shared_ptr<void>로도 원래 타입을 정확하게 삭제 가능하다.
Takeaways
std::function
- 여러 타입을 “호출 가능한 객체”로 추상화하는 컨테이너
- Concept–Model–Container 구조를 이용해 타입 정보를 숨긴다
- overhead: virtual call + heap alloc
- 하지만 사용성은 강력함
std::shared_ptr
shared_ptr<void>가 안전하게 동작하는 이유는
→ 메모리 블록 삭제 시 원래 객체 타입의 deleter를 호출하기 때문- 이 deleter 또한 virtual 기반 Type Erasure 구조
unique_ptr<void>에서 delete가 불가능한 이유도 같은 원리
- Type Erasure는 C++ 표준 라이브러리 곳곳에서 핵심적인 역할을 한다.
- “이질적인 타입들을 공통 인터페이스로 다루기” 위해 사용하는 강력한 패턴.
- std::function / std::shared_ptr / std::any / std::packaged_task 등에서 널리 사용됨.
Reference
Unveiling C++ Type Erasure - From std::function to std::any - Sarthak Sehgal - C++Online 2025 CppNorth 2025, Sarthak Sehgal - Unveiling Type Erasure in C++: From std::function to std::any
Type erasure — Part I, Andrzej’s C++ blog Type erasure — Part III, Andrzej’s C++ blog Type erasure — Part III, Andrzej’s C++ blog Type erasure — Part IV, Andrzej’s C++ blog