boost라이브러리 중에서 가장 많이 사용하는 기능인 BOOST_FOREACH()와 shared_ptr의 내부 구조를 분석합니다. 그리고 boost의 내부 구현에 사용된 이 기능을 프로그래밍에 응용하는 방법을 제시합니다.
* BOOST_FOREACH 구조 분석 및 응용
* shared_ptr 구조 분석 및 응용
1. BoostBoost 라이브러리의 내부 구조라이브러리의 내부 구조
에 사용된 기법을 응용하기에 사용된 기법을 응용하기
KOG, 서진택
jintaeks@gmail.com
2015 년 11 월 11 일
2. 발표자 , jintaeks@gmail.com
2000 년 ㈜ Kog 입사
– 프로그래머로 게임 개발 시작
~2015 년 9 월 ㈜ Kog 프로그래머
– 프로그래머로 게임 개발 및 기술 지원 프로그래밍
– 참여 게임
• 와일드랠리
• 범퍼킹재퍼
• 엘소드
• 얼티밋레이스
저서
– 게임개발자를 위한 C++, 민프레스 , 2003
– MFC 의 구조와 원리 , 한빛미디어 , 2005
2
3. 발표순서
base-from-member idiom
BOOST_FOREACH 의 내부 구조
– if-for combination idiom
– RAII idiom
– int-to-type idiom
– BOOST_TYPEOF
boost::shared_ptr 의 내부 구조
– copy-and-swap idiom
– safe-bool idiom
Q&A
3
이 발표는 boost 라이브러리에서 가장 많이
사용하는 BOOST_FOREACH() 와
shared_ptr 의 내부 구조를 분석합니다 .
그리고 바닥부터 해당 기능을 구현합니다 .
이 과정을 통해서 boost 가 사용한 기법들
을 프로그래밍에 응용하는 방법을 제시합
니다 .
4. base-from-member: 문제 상황
class KObject
{
public:
KObject()
{
m_iInitialized = 99;
}
int GetInitialized() const
{
return m_iInitialized;
}
private:
int m_iInitialized;
};//KObject
4
class KBase
{
public:
KBase( KObject& obj_ )
{
std::cout << obj_.GetInitialized() << std::endl;
}
};//class KBase
상속받은 클래스의 멤버를 베이스 클래스
가 접근하는 경우가 있습니다 .
KBase 의 생성자에서 m_object 의 멤버를
호출하는 것은 객체가 아직 초기화되지 않
았으므로 안전하지 않습니다 .
5. base-from-member: 해결방법
struct KPrivateBase
{
KObject m_object;
};//struct KPrivateBase
class KDerived2 : protected KPrivateBase, public KBase
{
public:
KDerived2() : KBase( m_object )
{
}
private:
//KObject m_object;
};//class KDerived
5
base-from-member idiom 은 문제를 해결하
기 위해 다중 상속에서 호출되는 클래스 생
성자의 순서를 이용합니다 .
boost 의 많은 곳에서는 , 베이스 클래스의
생성자에서 완전히 초기화된 객체를 접근
하기 위해 이 방법을 사용합니다 .
8. 관찰 : if 의 조건문에 사용한 변수
#include <iostream>
#include <algorithm>
#include <functional>
#include <vector>
#include <boost/foreach.hpp>
#include <boost/typeof/typeof.hpp>
void main()
{
if( int iBlockScope = 0 ) {}
else
{
iBlockScope = 3;
std::cout << iBlockScope << std::endl;
}
//std::cout << iBlockScope << std::endl; // invalid access
}//main()
8
if 문에 선언된 변수는 if 블록의 하부에서 사용
할 수 있습니다 . 또한 if 블록을 빠져 나가면 접
근할 수 없습니다 .
즉 if 문에 선언된 변수의 범위 scope 는 if- 블록
안으로 제한됩니다 .
변수를 true 가 아닌 값으로 초기화하면 , else-
블록에서 접근하는 것이 가능합니다 .
9. 관찰 : for 에 사용한 문장
#include <iostream>
void main()
{
if( int iBlockScope = 0 ) {}
else
for( printf( "hello worldrn" ); iBlockScope == 0; iBlockScope = 1 )
{
iBlockScope = 3;
std::cout << iBlockScope << std::endl;
}
/**
hello world
3
계속하려면 아무 키나 누르십시오 . . .
*/
}//main()
9
for 문의 첫번째 문장에는 조건에 상관없이
자유롭게 문장 statement 을 적을 수 있습
니다 . 또한 for 문에서 선언된 변수는
block scope 를 가집니다 .
10. 관찰 : { 와 } 사용이 가능한 RAII 매크로 정의
#include <iostream>
#define RAII( statement_ )
if( int iBlockScope = 0 ) {}
else
for( statement_; iBlockScope == 0; iBlockScope = 1 )
void main()
{
RAII( printf( "hello worldrn" ) )
{
iBlockScope = 3;
std::cout << iBlockScope << std::endl;
}//RAII()
/**
hello world
3
계속하려면 아무 키나 누르십시오 . . .
*/
}//main() 10
RAII 는 초기화 initialize 코드와 마무리
finalize 코드가 자동으로 호출되도록 하
는 디자인 패턴입니다 . RAII 는 코드를 안
전하고 깔끔하게 만듭니다 .
if-for 문장을 조합하면 , 블록 scope 를
가지는 변수를 , { 안에서만 사용하도록
매크로를 정의할 수 있습니다 .
11. int2type
template <int I>
struct Int2Type
{
enum { value = I };
};
template <class T, unsigned int N>
class Array : public std::array <T, N>
{
enum AlgoType { NOOP, INSERTION_SORT,
QUICK_SORT };
static const int algo = (N==0) ? NOOP
: (N==1) ? NOOP
: (N<50) ? INSERTION_SORT : QUICK_SORT;
void sort (Int2Type<NOOP>) {}
void sort (Int2Type<INSERTION_SORT>) {}
void sort (Int2Type<QUICK_SORT>) {}
public:
void sort()
{
sort (Int2Type<algo>());
}
11
int main(void)
{
Array<int, 1> a;
a.sort(); // No-op!
Array<int, 400> b;
b.sort(); // Quick sort
}
컴파일 시간에 정수 상수를 타입
으로 취급하도록 합니다 .
12. 관찰 : sizeof 에 표현식 사용
#include <iostream>
#include <vector>
#include <boost/typeof/typeof.hpp>
#include <boost/mpl/int.hpp>
template<class T>
T type_encoder( T expression_ )
{
return expression_;
}
template<int ID>
struct typeid_wrapper
{
typedef typename extract_type<int_<ID> >::id2type id2type;
typedef typename id2type::type type;
};
12
함수 템플릿은 파라미터로 전달
된 표현식의 타입을 자동으로
유추하는 역할로 사용할 수 있
습니다 .
id 로 구분되는 유일한 타입을 생성하고 ,
typeid_wrapper<>::type 으로 접근합니다 .
13. 관찰 : BOOST_TYPEOF 의 원리
#include <iostream>
#include <vector>
#include <boost/typeof/typeof.hpp>
#include <boost/mpl/int.hpp>
…
void main()
{
int i = 1;
sizeof( type_encoder(i+3) );
typeid_wrapper<sizeof( type_encoder(i+3) ) >::type k;
BOOST_TYPEOF( i + 3 ) j;
j = 3;
std::cout << i << std::endl;
std::cout << j << std::endl;
}//main()
13
sizeof() 에는 type 뿐만 아니라 unary
expression 을 전달할 수 있습니다 .
함수 호출과 sizeof() 를 이용하면 표현식을
템플릿에 전달할 수 있습니다 .
이런 원리를 사용하여 구현된 , BOOST_TYPEOF() 는
표현식의 타입을 구합니다 ! i+3 의 타입이 int 이므로
int j; 와 같은 문장입니다 .
14. 응용 예 : KLOCK_BEGIN
KLOCK_DECLARE_INIT( KTest, m_int, (0) );
void main()
{
KLOCK_DECLARE( std::vector<int>, vecUserInfo );
std::cout << vecUserInfo.size() << std::endl;
//vecUserInfo.push_back( 3 ); // compile time error
//m_int.SetInt( 5 ); // compile time error
KLOCK_BEGIN( m_int )
KLOCK_BEGIN( vecUserInfo )
{
m_int.SetInt( 5 );
vecUserInfo.push_back( 5 );
}
std::cout << m_int.GetInt() << std::endl;
std::cout << vecUserInfo.size() << std::endl;
}
14
특정한 멤버 변수를 읽는 것은 허용하
되 , 쓰는 동작은 에러가 나도록 할 수
있습니다 .
KLOCK_DECLARE() 로 선언한 변수는
이러한 특징을 가지도록 합니다 .
KLOCK_BEGIN() 으로 특정한 변수에
대한 쓰기 권한을 허용할 수 있습니다 .
이러한 기법은 읽기와 쓰기 동작을 구
분하는 코드를 작성하도록 합니다 .
변수를 변경할 때 실행되어야 하는 코
드를 지정함으로써 , 여러가지
housekeeping 동작을 숨길 수 있습니
다 .
21. 응용 예 : ASSET_FOREACH
기본적으로 구현한 FOREACH() 를 응용하여 , 프로그
램의 내부 로직에 의존적인 여러가지 매크로를 정의
할 수 있습니다 .
– 프로그램에서 현재 관리중인 모든 asset 들에 대한 커스텀
iteration 을 작성할 수 있습니다 .
– .xml 파일을 로드하고 나서 , 모든 element 에 대한 커스텀
iteration 을 작성할 수 있습니다 .
– 프로그램에서 현재 생성된 모든 게임 객체에 대한 커스텀 iteration
을 작성할 수 있습니다 .
21
23. void 참조를 특화로 구현
23
template<class T> struct shared_ptr_traits
{
typedef T& reference;
};
template<> struct shared_ptr_traits<void>
{
typedef void reference;
};
void 타입에 대한 reference 가 가능하도록
템플릿 특화 specialization 을 사용하여 미
리 구현해 둡니다 .
기타 type-trait 에 대한 설명은 생략합니다 .
24. shared_ptr: 생성자 구현
24
template<class T> class shared_ptr
{
public:
typedef shared_ptr<T> this_type;
typedef T value_type;
typedef T* pointer;
typedef typename shared_ptr_traits<T>::reference reference;
public:
shared_ptr(T * p = 0) : px(p), pn(0)
{
if( px != NULL )
pn = new int(1);
}
생성자에서는 전달받은 raw pointer 를 초
기화합니다 .
참조 카운팅을 위한 메모리 할당을 하고 ,
참조 카운터를 1 로 초기화합니다 .
25. shared_ptr: 복사 생성자와 파괴자 구현
25
shared_ptr( const shared_ptr& right_ ) : px( 0 ), pn( 0 )
{
release();
px = right_.px;
pn = right_.pn;
if( px != NULL )
*pn += 1;
}
~shared_ptr()
{
release();
}
복사 생성자는 먼저 이전에 할당되어 있는
리소스를 해제 합니다 .
새로운 리소스에 대한 참조를 설정하고 ,
참조 카운터를 증가시킵니다 .
33. 관찰 : if 의 조건문 표현식
33
class KBoolTest
{
public:
operator bool() const { return true; }
operator int() const { return 1; }
operator int*() const { return NULL; }
int GetValue() const { return 9; }
};
int main()
{
KBoolTest t;
if( t )
std::cout << t.GetValue() << std::endl;
return 0;
}//int main()
if 문의 조건은 일반적인 bool 표현식이 아닌
C 가 허용하는 참인 조건식을 적을 수 있습니다
.
평가의 partial order 의 순서는 bool native
type pointer 의 순입니다 .
34. safe bool idiom
34
//int* p = spInt; // (1)
//if( spInt < spInt ) // (2)
//{
//} 지금까지의 구현은 (1) 과 (2) 처럼 잘못된 사
용이나 , 의미없는 bool 표현식을 막지 못
합니다 .
template <typename T> void some_func(const T& t) {
if (t)
t->print();
}
smart pointer T 에 대해 이 문장이 동작
하려면 T 는 bool 형에 대한 형 변환 연
산자를 제공해야 합니다 .
35. 해결시도 1: operator bool() 구현
35
class Testable {
bool ok_;
public:
explicit Testable(bool b=true):ok_(b) {}
operator bool() const {
return ok_;
}
}; shared_ptr 은 자신이 valid 한 raw pointer 를 가질 때 ,
true 를 리턴하는 operator bool() 을 작성할 수 있습니다 .
36. operator bool() cont.
36
test << 1; // (1)
int i=test; // (2)
Testable a;
AnotherTestable b;
if (a==b) { // (3)
}
if (a<b) { // (4)
}
이 구현은 (1), (2) 같은 의미 없는 문장
이나 , (3), (4) 같은 의미 없는 bool 검
사를 막지 못합니다 .
37. 해결시도 2: operator void*() 구현
37
operator void*() const {
return ok_==true ? this : 0;
}
이 구현은 영리해 보이지만 , 아래의 문장처럼
delete 를 직접호출 할 수 있는 약점을 가집니다 .
Testable test;
delete test;
38. 해결시도 3: nested class
38
class Testable {
bool ok_;
public:
explicit Testable(bool b=true):ok_(b) {}
class nested_class;
operator const nested_class*() const {
return ok_ ? reinterpret_cast<const nested_class*>(this) : 0;
}
};
Testable b1,b2;
if (b1==b2) {
}
if (b1<b2) {
}
safe bool 을 위한 nested class 구현 역시 의미없는 bool 검
사를 막지 못합니다 .
이 문제를 해결하려면 포인터 형변환이 되지만 , < 같은 비교
연산자를 지원하지 않는 데이터 타입을 사용하는 것입니다 .
흥미롭게도 멤버 함수에 대한 포인터가 그렇게 동작합
니다 !
39. 최종 버전 : safe bool idiom
39
class Testable {
bool ok_;
typedef void (Testable::*bool_type)() const;
void this_type_does_not_support_comparisons() const {}
public:
explicit Testable(bool b=true):ok_(b) {}
operator bool_type() const {
return ok_==true ?
&Testable::this_type_does_not_support_comparisons : 0;
}
};
if- 문장의 조건 표현식에 포인터가 사용될 수
있는 점을 이용하여 , 함수 포인터를 리턴하
는 연산자 함수를 정의합니다 .
40. shared_ptr 의 내부 : unspecified_bool_type 사용
40
//operator pointer() const
//{
// return px;
//}
void unspecified_bool() const
{
}
typedef void (shared_ptr::*unspecified_bool_type)() const;
operator unspecified_bool_type() const // never throws
{
return px == 0 ? 0 : &shared_ptr::unspecified_bool;
}
boost 의 실제 코드는
unspecified_bool_type() 을 사용합니다 .
C++ 표준 라이브러리 뿐만 아니라 , 게임
엔진의 스마트 포인터 구현은 모두 이러한
기법을 사용합니다 .
41. shared_ptr 에 적용된 safe bool idiom
41
typedef shared_ptr<int> IntPtr;
IntPtr spInt = new int(3);
IntPtr spInt2 = spInt;
IntPtr spInt3 = spInt;
spInt.reset( new int(4) );
*spInt = 5;
if( spInt2 != NULL )
{
std::cout << *spInt << std::endl;
}//if
int* p = spInt; // (1) error
if( spInt2 < spInt3 ) // (2) error
{
}
이제 (1) 과 (2) 같은 의미 없는 문
장은 컴파일 시간 에러가 발생합
니다 .
42. implicit constructor 문제 해결하기
42
void Test( shared_ptr<int> spInt_ )
{
}
int iData = 5;
Test( &iData );
실제 포인터가 아닌 , 포인터 표현식에 대한
shared_ptr 생성을 막을 필요가 있습니다 .
이 문제에 대한 해결방법은 명시적 생성만 가능하도록
클래스를 설계하는 것입니다 .
explicit shared_ptr(T * p = 0) : px(p), pn(0)
{
if( px != NULL )
pn = new int(1);
}
45. weak_ptr<T> cont.
45
class TestRef
{
public:
TestRef(){}
void SetTest( TestPtr tp )
{
m_tp = tp;
}
void Print() const
{
if( TestPtr tp = m_tp.lock() )
{
tp->Print();
}
}
private:
TestWeakPtr m_tp;
};
weak_ptr 을 사용하는 경우 , 지금 사용해
도 안전한 실제 포인터를 얻기 위해서
lock() 을 호출합니다 .
int main()
{
TestPtr tp = Test::CreateTest( 1 );
TestRef ref;
ref.SetTest( tp );
tp.reset();
ref.Print();
}//int main()
46. weak_ptr<T> lock()
46
weak_ptr<T> 의 lock() 구현
shared_ptr<T, A, D> lock() const // never throws
{
// optimization: avoid throw overhead
if(expired()){
return shared_ptr<element_type, A, D>();
}
BOOST_TRY{
return shared_ptr<element_type, A, D>(*this);
}
}
shared_ptr<T>( weak_ptr<Y> ) 구현
template<class Y>
explicit shared_ptr(weak_ptr<Y> const & r): pn(r.pn) // may throw
{
// it is now safe to copy r.px, as pn(r.pn) did not throw
px = r.px;
}
47. raw pointer 에서 shared_ptr 얻기 : factory
47
class X
{
private:
X() { ... }
public:
static shared_ptr<X> create()
{
shared_ptr<X> px(new X);
// use px as 'this_'
return px;
}
};
shared_ptr<T> 로 관리되지 않는 raw 포
인터에서 shared_ptr<T> 을 얻을 필요가
있습니다 .
클래스 객체가 shared_ptr<T> 로 관리되
더라도 , 생성자에서 shared_ptr<T> 를 얻
을 방법도 없습니다 .
해결 방법은 weak_ptr 과 팩토리 함수를
사용하는 것입니다 .
48. factory: this 에 대한 shared_ptr<T>
48
class impl: public X, public Y
{
private:
weak_ptr<impl> weak_this;
impl(impl const &);
impl & operator=(impl const &);
impl() { ... }
public:
static shared_ptr<impl> create()
{
shared_ptr<impl> pi(new impl);
pi->weak_this = pi; // (1) 팩토리에서 weak_ptr<T> 을 초기화 해 놓습니다 .
return pi;
}
virtual void f() { ... }
virtual shared_ptr<X> getX()
{
shared_ptr<X> px(weak_this);
return px;
}
};
대상 객체를 생성할 때 , weak_ptr 이 적절
하게 초기화되도록 합니다 .
그러면 , 후에 필요하면 대상 객체에 대한
shared_ptr 을 얻을 수 있습니다 .
49. boost::enable_shared_from_this<T>
49
#include <iostream>
#include <boost/shared_ptr.hpp>
#include <boost/enable_shared_from_this.hpp>
class Test;
typedef boost::shared_ptr<Test> TestPtr;
class Test : public boost::enable_shared_from_this<Test>
{
public:
static TestPtr CreateTest(int i)
{
TestPtr sp( new Test( i ) );
return sp;
}
Test(int i) : m_i(i) {}
void Print() const
{
std::cout << m_i << std::endl;
}
private:
int m_i;
};
이 역할을 하는 boost 의 베이스 클래스가
enabled_shared_from_this 입니다 .
50. shared_from_this()
50
void TestFun( Test* pt )
{
TestPtr tp;
if( pt != NULL )
tp = pt->shared_from_this();
if( tp != NULL )
{
tp->Print();
}
}
Test 가 enable_shared_from_this 를 상속받은 경ㅇ
우 , raw pointer 에서 shared_ptr 을 얻기 위해서는
shared_from_this() 를 호출합니다 .
51. 응용 예 : RAII
51
#include <boost/shared_ptr.hpp>
#include <boost/enable_shared_from_this.hpp>
#include <boost/typeof/typeof.hpp>
#define KSmartPointer(type_)
class type_;
typedef boost::shared_ptr<type_> type_##SharedPtr;
typedef boost::weak_ptr<type_> type_##WeakPtr;
#define IF_MEMBER_WEAKPTR( member_name_ )
if( boost::shared_ptr<BOOST_TYPEOF(member_name_)::element_type> member_name_ = this-
>member_name_.lock() )
게임 객체가 관리되는 리소스가 있는 경우에만 사용하려
고 하는 경우 , 리소스에 대한 weak_ptr 을 멤버로 가집니
다 .
게임 객체가 실제 객체를 사용하려고 하면 , lock() 호출이
필요합니다 .
이러한 패턴을 RAII 매크로로 작성했습니다 .
52. 응용 예 cont.
52
class TestContainer
{
public:
TestContainer(){}
void SetTest( TestSharedPtr tp )
{
m_tp = tp;
}
void Print() const
{
IF_MEMBER_WEAKPTR( m_tp )
{
m_tp->Print();
}//if
}
private:
TestWeakPtr m_tp;
};
IF_MEMBER_WEAKPTR() 은 RAII 스타일의 매크
로로 멤버에 대한 안전한 접근을 보장합니다 .