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_ptr
  • ControlBlockBase* 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>로도 원래 타입을 정확하게 삭제 가능하다.

Full Example Code


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

Back to Basics: Type Erasure - Arthur O’Dwyer - CppCon 2019