사전 지식 1. Empty Class

개념

  • instance(non-static) 멤버 데이터가 없는 클래스
  • std::is_empty<type>::value (C++11)
  • std::is_empty_v (C++17)

Empty Class 조건

  • static 멤버, 일반 멤버 함수는 있어도 empty
  • 가상 함수, 가상 상속 있으면 안 됨
  • empty class를 멤버로 가지고 있어도 안 됨

사용 사례

  • 상태가 없는 함수 객체
  • 캡처하지 않는 lambda
  • 메모리 할당의 allocator
  • Tag-Dispatching

사전 지식 2. Tag-Dispatching

개념

  • Empty class를 함수 오버로딩에 사용하는 기술
  • 암시적 형 변환을 막기 위해 explicit 생성자 사용

상황

  • lock_guard라고 mutex를 관리하기 위한 클래스가 있는데 생성자에서 mutex lock을 함
  • 그런데 이미 lock한 mutex를 넘겼을 때 락을 안하게 하기 위해 따로 state variable을 사용하지않고 empty 클래스를 이용해서 함수 오버로딩을 한다.

예시

  #include <iostream>
#include <mutex>

struct adopt_lock_t {
    explicit adopt_lock_t() = default;
};

constexpr adopt_lock_t adopt_lock;

template <class Mutex>
class lock_guard {
public:
    using mutex_type = Mutex;
    explicit lock_guard(Mutex& mtx) : mtx(mtx) { mtx.lock(); }
    explicit lock_guard(Mutex& mtx, adopt_lock_t) : mtx(mtx) { }
    ~lock_guard() noexcept { mtx.unlock(); }

    lock_guard(const lock_guard&) = delete;
    lock_guard& operator=(const lock_guard&) = delete;
private:
    Mutex& mtx;
};

std::mutex m1, m2;

int main() {
    m2.lock();
    lock_guard g1(m1);
    lock_guard g2(m2, adopt_lock);
}
  

new(std::nothrow), unique_lock u1(m, std::adopt_lock) 같은 방식 사용

사전 지식 3. Empty Base Class Optimization (EBCO)

개념

  • Empty Class를 멤버로 → 1 byte + padding(3 byte)
  • Empty Class를 상속 → 크기 0 byte로 최적화

그러므로 가능하면 상속으로 처리하면 이득

예시

  #include <iostream>     
#include <type _traits> 

class Empty {};

template<typename T1, typename T2, bool = std::is_empty_v<T1>>
struct PAIR;

template<typename T1, typename T2>
struct PAIR<T1, T2, false> {
    T1 first;
    T2 second;
};

template<typename T1, typename T2>
struct PAIR<T1, T2, true> : public T1 {
    T2 second;
};

int main() {
    PAIR<int, int> p1;
    PAIR<Empty, int> p2;
    std::cout << sizeof(p1) << std::endl; // 8
    std::cout << sizeof(p2) << std::endl; // 4
}
  

compressed_pair

구현 예시

  • 유의할 것은 Boost 라이브러리에서 제공해주는 compressed_pair와 달리 생성자 인자를 넘기는 방식을 지원하기 위해 tag-dispatch 사용한다는 점이 차이점이며 template의 false specialization 버전만 구현함
  • 다음 예제에서 no_unique_address와 std::in_place_t를 활용하여 개선
  class Empty {};
struct one_and_variadic_arg_t { explicit one_and_variadic_arg_t() = default;};
struct zero_and_variadic_arg_t { explicit zero_and_variadic_arg_t() = default;};  g~   

template<typename T1, typename T2, bool = std::is_empty_v<T1> && !std::is_final_v<T1> >
struct compressed_pair; // Primary

template<typename T1, typename T2>
struct compressed_pair<T1, T2, false> // empty
{
    T1 first;
    T2 second;

    constexpr T1& getFirst() noexcept { return first; } // constexpr, noexcpet
    constexpr T2& getSecond() noexcept { return second; }
    constexpr const T1& getFirst() const noexcept { return first; }
    constexpr const T2& getSecond() const noexcept { return second; }

    template<typename A1, typename ... A2>
    constexpr compressed_pair( one_and_variadic_arg_t, A1&& arg1, A2&& ...arg2) noexcept(
    std::conjunction_v< std::is_nothrow_constructible<T1, A1>, std::is_nothrow_constructible<T2, A2...> >
    )
    : first( std::forward<A1>(arg1) ), second( std::forward<A2>(arg2)... ) {}


    template<typename ... A2>
    constexpr compressed_pair( zero_and_variadic_arg_t, A2&& ... arg2) noexcept(
    std::conjunction_v< std::is_nothrow_default_constructible<T1>, std::is_nothrow_constructible<T2, A2...> > )
    : first(), second( std::forward<A2>(arg2)... ) {}
};

compressed_pair<int, Point> cp1( one_and_variadic_arg_t{}, 1, Point(0,0));
compressed_pair<int, Point> cp2( one_and_variadic_arg_t{}, 1, 0, 0);
  

no_unique_address

개념

  • no_unique_address: Empty class 멤버가 독립적인 주소를 가질 필요 없음을 명시하여 크기에 포함되지 않게 됨
  • C++20에서 추가된 attribute
  • 같은 타입 두 개면 여전히 크기 가짐
  • 이를 활용하여 compress_pair 개선. true, false 상관없이 구현
  #include <iostream>
#include <type_traits>

template<typename T1, typename T2>
struct compressed_pair {
    [[no_unique_address]] T1 first;  // empty일 경우 메모리 차지 안 함
    T2 second;

    constexpr T1& getFirst() noexcept { return first; }
    constexpr T2& getSecond() noexcept { return second; }
    constexpr const T1& getFirst() const noexcept { return first; }
    constexpr const T2& getSecond() const noexcept { return second; }

    template<typename A1, typename... A2>
    constexpr compressed_pair(A1&& arg1, A2&&... arg2)
        noexcept(std::conjunction_v<std::is_nothrow_constructible<T1, A1>,
                                    std::is_nothrow_constructible<T2, A2...>>)
        : first(std::forward<A1>(arg1)), second(std::forward<A2>(arg2)...) {}

    template<typename... A2>
    constexpr explicit compressed_pair(std::in_place_t, A2&&... arg2)
        noexcept(std::conjunction_v<std::is_nothrow_default_constructible<T1>,
                                    std::is_nothrow_constructible<T2, A2...>>)
        : first(), second(std::forward<A2>(arg2)...) {}
};

struct Empty {};

struct Point {
    int x, y;
    Point(int x = 0, int y = 0) : x(x), y(y) {}
};

int main() {
    compressed_pair<int, Point> cp1(1, Point(1,2));
    compressed_pair<Empty, Point> cp2(Empty{}, Point(3,4));

    std::cout << "sizeof(cp1): " << sizeof(cp1) << std::endl;
    std::cout << "sizeof(cp2): " << sizeof(cp2) << std::endl;

    return 0;
}
  

unique_ptr 구현

기본 구조

  #include <iostream>

template<typename T> struct default_delete {
    void operator()(T* p) const {
        std::cout << "delete" << std::endl;
        delete p;
    }
};

template <typename T, typename D = default_delete<T>>
class unique_ptr {
    compressed_pair<D, T*> cpair;
public:
    explicit unique_ptr(T* p) : cpair(zero_and_variadic_arg_t{}, p) {}
    unique_ptr(T* p, const D& d) : cpair(one_and_variadic_arg_t{}, d, p) {}
    unique_ptr(T* p, D&& d) : cpair(one_and_variadic_arg_t{}, std::move(d), p) {}
    ~unique_ptr() {
        if (cpair.getSecond()) cpair.getFirst()(cpair.getSecond());
    }

    T& operator*() const { return *cpair.getSecond(); }
    T* operator->() const { return cpair.getSecond(); }
};
  

사용 예시

  int main() {
    unique_ptr<int> p1(new int);
    auto del = [](int* p) { free(p); };
    unique_ptr<int, decltype(del)> p2(static_cast<int*>(malloc(sizeof(int))), del);
    std::cout << sizeof(p1) << std::endl;
    std::cout << sizeof(p2) << std::endl;
}
  

unique_ptr 최종 구현

  #include <iostream>
#include <algorithm>
#include <utility>

template<typename T> struct default_delete {
    void operator()(T* p) const { delete p; }
};

template<typename T1, typename T2> struct compressed_pair {
    T1 first;
    T2 second;

    constexpr T1& getFirst() noexcept { return first; }
    constexpr T2& getSecond() noexcept { return second; }
};

template <typename T, typename D = default_delete<T>>
class unique_ptr {
public:
    using pointer = T*;
    using element_type = T;
    using deleter_type = D;

    unique_ptr() noexcept : cpair{} {}
    explicit unique_ptr(pointer p) noexcept : cpair{ {}, p } {}
    unique_ptr(pointer p, const D& d) noexcept : cpair{ d, p } {}
    unique_ptr(pointer p, D&& d) noexcept : cpair{ std::move(d), p } {}
    ~unique_ptr() noexcept {
        if (cpair.getSecond()) cpair.getFirst()(cpair.getSecond());
    }

    T& operator*() const noexcept { return *cpair.getSecond(); }
    pointer operator->() const noexcept { return cpair.getSecond(); }
    pointer get() const noexcept { return cpair.getSecond(); }
    D& get_deleter() noexcept { return cpair.getFirst(); }
    const D& get_deleter() const noexcept { return cpair.getFirst(); }
    explicit operator bool() const noexcept { return static_cast<bool>(cpair.getSecond()); }

    pointer release() noexcept {
        pointer old = cpair.getSecond();
        cpair.getSecond() = nullptr;
        return old;
    }

    void reset(pointer ptr = nullptr) noexcept {
        pointer old = std::exchange(cpair.getSecond(), ptr);
        if (old) cpair.getFirst()(old);
    }

    void swap(unique_ptr& up) noexcept {
        std::swap(cpair.getFirst(), up.cpair.getFirst());
        std::swap(cpair.getSecond(), up.cpair.getSecond());
    }

    unique_ptr(const unique_ptr&) = delete;
    unique_ptr& operator=(const unique_ptr&) = delete;

    unique_ptr(unique_ptr&& up) noexcept
        : cpair(std::move(up.cpair.getFirst()), up.release()) {}

    unique_ptr& operator=(unique_ptr&& up) noexcept {
        if (this != std::addressof(up)) {
            reset(up.release());
            cpair.getFirst() = std::move(up.cpair.getFirst());
        }
        return *this;
    }
private:
    compressed_pair<D, pointer> cpair;
};

int main() {
    unique_ptr<int> p1;
    unique_ptr<int> p2(nullptr);
    unique_ptr<int> p3 = nullptr;
    unique_ptr<int> up1(new int);
    unique_ptr<int> up2 = std::move(up1);
}
  

결론

unique_ptrRAII를 구현한 스마트 포인터로, 메모리 해제 책임을 자동화하며 사용자 정의 deleter, EBCO 최적화까지 포함해 메모리/코드 효율성을 높인다.