Empty class를 활용한 Tag dispatching
STL에서 자주 사용하는 기법 중에 Tag dispatching이 있다. Tag dispatching이란 Empty class를 활용한 오버로딩 기법이다. 실행시간에 조건에 따라서 A 함수를 실행할지 B함수를 실행할지 결정하기 위해서 if 문을 사용할 수 있다. 하지만, if문의 활용이 여의치 않을 경우 Tag dispatching 기법을 활용할 수 있다.
Empty class
먼저 Tag dispatching에 사용되는 Empty class를 살펴보자. Empty class란 상태값(non static 멤버 변수)을 가지지 않는 비가상 클래스를 말한다. 즉, 객체를 생성하는 데 있어서 메모리에 적재될 내용이 없는 클래스이다. 그리고 Empty class의 객체는 비록 멤버 변수는 없지만 메모리 어딘가에 저장되어야 하기 때문에 1byte의 크기를 가진다. Empty class는 Tag dispatching에도 사용되지만, 상태 값이 필요 없는 std::allocator나 함수 객체, 람다도 empty class에 해당한다.
class E1 {
// empty class
};
class E2 {
void foo(); // 멤버함수를 가져도 empty
static int a; // static 멤버를 가져도 객체가 생성될 때 적재되는 것이 아니기때문에 empty
};
class NE1 {
int a; // 멤버변수를 가졌기 때문에 empty가 아님
};
class NE2 {
virtual void foo(); // 가상함수 포인터 생성되기 때문에 empty가 아님
};
int main()
{
std::cout << sizeof(E1) << std::endl;
std::cout << sizeof(E2) << std::endl;
std::cout << sizeof(NE1) << std::endl;
std::cout << sizeof(NE2) << std::endl;
return 0;
}
Tag dispatching
Tag dispatching이 사용되는 예시로 lock guard를 살펴보자. lock guard는 RAII 기법을 활용해서 자동으로 lock과 unlock을 해주는 객체이며, 대략 아래와 같이 구현되어 있다.
#include <mutex>
template <class Mutex>
class lock_guard
{
public:
explicit lock_guard(Mutex& inMutex) : mutex(inMutex) { mutex.lock(); }
~lock_guard() noexcept { mutex.unlock(); }
private:
Mutex& mutex;
};
int main()
{
{
std::mutex m; // !!아직 lock을 걸지 않은 mutex!!
lock_guard<std::mutex> g(m);// lock()
// unlock()
}
return 0;
}
일반적으로는 lock_guard 생성자를 통해서 mutex의 lock을 걸지만 상황에 따라서 이미 lock을 했지만, unlock만 lock_guard를 활용하고 싶을 수 있다. 하지만 위의 lock_guard 그대로 사용할 경우 lock()이 두번 걸리는 문제가 발생한다.
{
std::mutex m;
m.lock();
lock_guard<std::mutex> g(m); // lock()이 한번 더 걸림!
// unlock()
}
이런 경우 두 가지 방법을 고려할 수 있다.
- 조건문 활용
- empty class를 활용한 오버로딩: Tag dispatching
조건문 활용
lock_guard 생성자에 lock 여부를 결정하는 bool타입 조건 인자를 받아서 해결할 수 있다. 하지만 이 경우 두 가지 단점이 존재한다.
- if 문을 활용할 경우 실행시간에 분기를 판단, 결정하기 때문에 컴파일 최적화를 활용할 수 없다. (성능 이슈)
- 매개변수에 따라서 생성자의 초기화 리스트값을 다르게 결정하기 어렵다.
#include <mutex>
template <class Mutex>
class lock_guard
{
public:
explicit lock_guard(Mutex& inMutex, bool autolock = true)
: mutex(inMutex) // 매개변수에 따라서 초기화 값을 다르게 설정하는 것은 어려움
{
if(autolock) mutex.lock(); // 실행시간에 분기작업.
}
~lock_guard() noexcept { mutex.unlock(); }
private:
Mutex& mutex;
};
int main()
{
{
std::mutex m;
m.lock();
lock_guard<std::mutex> g(m, false); // 생성자에서 lock()을 걸지 않는다.
// unlock()
}
return 0;
}
Tag dispatching
Empty class를 매개변수로 받는 새로운 lock_guard 생성자를 오버로딩 해서 문제를 해결할 수 있다. 이때 Empty class는 그 역할을 알 수 있도록 설명해주는 네이밍을 하면 의미 전달이 더욱 분명 해지는 효과도 얻을 수 있다.
#include <mutex>
struct adopt_lock_t
{
explicit adopt_lock_t() = default;
};
constexpr adopt_lock_t adopt_lock; //empty class의 객체
template <class Mutex>
class lock_guard
{
public:
explicit lock_guard(Mutex& inMutex) : mutex(inMutex) { mutex.lock();}
explicit lock_guard(Mutex& inMutex, adopt_lock_t) : mutex(inMutex) {} // lock() 호출하지 않는 생성자 오버로딩
~lock_guard() noexcept { mutex.unlock(); }
private:
Mutex& mutex;
};
int main()
{
{
std::mutex m;
m.lock();
lock_guard<std::mutex> g(m, adopt_lock); // 생성자에서 lock()을 걸지 않는다.
// unlock()
}
return 0;
}
위와 같이 Empty class를 오버로딩을 위해서 사용하는 기법을 Tag dispatching이라고 한다.