On this page
article
C++ Make Unique Ptr
사전 지식 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_ptr
은 RAII
를 구현한 스마트 포인터로, 메모리 해제 책임을 자동화하며 사용자 정의 deleter, EBCO 최적화까지 포함해 메모리/코드 효율성을 높인다.