• 2006-06-19

    전통적인 FPS 게임의 네트워킹 구현

    한국 대 프랑스전을 기다리면서 하프라이프2 위키 중에서 Source Multiplayer Networking 부분을 요약 정리해보았다.

    하프라이프2 서버와 클라이언트

    퀘이크나 하프라이프2(이하 HL2) 같은 클라이언트-서버 기반의 FPS 게임에서, 클라이언트는 기본적으로 렌더링 더미(dummy)라고 봐야 한다. 왜냐하면 클라이언트가 키보드나 마우스 입력을 샘플링해서 서버로 보내면, 서버는 초당 30-40회의 시뮬레이션을 거친 후 그 결과(snapshot)를 주기적으로 브로드캐스트하며, 클라이언트는 스냅샷을 받아서 렌더링을 하는 방식으로 구현되기 때문이다. 대신 서버는 보안적인 이유 때문에 대부분의 이동, 공격, 인증, 보안 체크를 해야 한다. (참고로, HL2 서버는 초당 33회의 시뮬레이션을 거치고 20회의 스냅샷을 브로드캐스트하며, 클라이언트는 초당 20회의 입력샘플링을 한다.)

    문제는 사용자의 입력 결과가 도착해서 실제로 그려지기까지의 시간이 꽤 오래 걸린다는 점이다. 이 랙(lag)을 해결하기 위해서 입력 예측(Input Prediction)이 사용되는데, 이는 내 캐릭터만큼은 서버의 결과를 기다리지 않고 먼저 움직이게 한다는 것이다. 이를 위해서 이동에 관련된 시뮬레이션 로직을 서버와 클라이언트 모두에 탑재해야 하며, 클라이언트는 서버에 입력을 보내자마자 로컬 시뮬레이션을 거쳐 내 캐릭터를 이동시킨 후, 서버에서 도착한 결과가 너무 차이가 많이 날 경우에만 보정을 하게 된다.

    Lag Compensation

    그런데 이것만으로 제대로된 FPS를 구현하기엔 조금 부족하다. 내가 보는 다른 사람의 위치는 언제나 과거이기 때문에, 명중 판정이 매우 곤란해지기 때문이다. (특히 퀘이크 시리즈의 레일건처럼 발사와 동시에 판정이 일어나야 하는 무기의 명중 판정을 서버에서 해야 한다고 상상해보라.) HL2에서는 이를 랙 보상(Lag Compensation) 기법으로 해결하는데, 서버에서 클라이언트의 위치 정보의 히스토리를 갖고 있다가 공격 패킷이 도착하면 발사 시간에서의 목표의 과거 위치를 찾아서 명중 체크를 한다는 것이다.

    Entity Interpolation

    이 정도면 정상적인 네트워크에서의 기본적인 이동과 전투 까지는 해결할 수 있지만, 만약 패킷 손실을 고려하지 않으면 불안한 네트워크에서 캐릭터가 마구 점프하는 모습을 보게 될 것이다. 물론 적절한 데드 레커닝으로 해결할 수도 있겠지만, HL2 클라이언트에서는 서버가 보낸 스냅샷을 즉시 처리하는 대신 일정 시간(100ms)동안 버퍼링을 한 후, 내삽(interpolation)과 외삽(extrapolation)을 이용해서 패킷 손실에 우아하게 처리한다. 위 그림을 보면 짐작하겠지만, 최소한 2개의 패킷을 버퍼링함으로써 둘 중 하나가 손실되더라도 나머지 패킷으로 추정이 가능하다.

    결국 전통적인 FPS 게임에서의 서버는, 단순히 패킷을 브로드캐스팅하는 릴레이 서버가 아니라, 이동-공격-판정-충돌 등 대부분의 로직을 처리해해야 하기 때문에, 생각보다 꽤 무거워질 수 밖에 없다. 반면 클라이언트는 자기 캐릭터를 제외한 대부분의 입력을 네트워크로부터 받아들이는 구조로 작성되어야 한다. 만약 여러분이 다른 FPS게임과는 달리 플레이어들에게 dedicated server를 배포할 수 없는 상황이라면, 여러분은 대역폭을 무한히 절약해야 하는 꽤 힘든 싸움을 시작한 것이다. Welcome to the hell, guys.. :)

  • 2006-06-08

    Lua Tips

    LUA는 현존하는 가장 가볍고 빠른 스크립트 언어로, 발더스게이트1과 월드 오브 워크래프트에서 사용되고 있다.

    LuaPack

    바이너리 데이터를 pack/unpack 하는 라이브러리. 일단 require 해주면 string.pack, string.unpack 의 2개 함수가 추가된다. 레이옷은 '''순수 루아 테스트 클라이언트'''를 만들기 위해서 LuaSocket 과 함께 사용했다. 단, 리눅스에서 사용하던 놈이라, 윈도우 환경에서 dll 로 사용하기 위해서는 약간의 수고가 필요하다.

    • 기존 루아 프로젝트를 dll 기반으로 수정해줘야 한다. 또한, 같이 배포되는 pack.lua 은 동작하지 않으며, LuaSocket 의 compat-5.1.lua 에서 제공하는 LUA_CPATH 및 새 require() 를 이용해야 한다.
    • lpack.c 의 luaopen_pack() 앞에 __declspec(dllexport) 달아줄 것
    • dll 이름과 luaopen_xxx() 을 동일하게 해줄 것. pack.dll & luaopen_pack() 또는 lpack.dll & luaopen_lpack()을 권장함
    • 다음과 같은 배치 파일 및 테스트용 루아를 실행, 2개 함수가 function:xxxxx 이라고 나오면 성공~
    # test_luapack.bat
    set LUA_CPATH=?.dll
    lua test_luapack.lua
    pause
    
    -- test_luapack.lua
    dofile('compat-5.1.lua')
    require("lpack")
    print(string.pack)
    print(string.unpack)
    

    변환표








    type string 의미 z zero-terminated string p string preceded by length byte
    P string preceded by length word
    a string preceded by length size_t
    A string
    f float
    d double
    n Lua number
    c char
    b byte = unsigned char
    h short
    H unsigned short
    i int
    I unsigned int
    l long
    L unsigned long
    < little endian
    > big endian|
    = native endian

    see also:

    ipairs()

    lua table 을 array 로 쓰는 경우, 정수 인덱스 기반의 iterating 을 할 때 사용한다. 정수 인덱스에 관련된 값이 nil 일 때까지 루프를 돌게 된다. 반면에 lua table 을 map 으로 사용할 경우, 즉 key 와 value 를 명시해서 사용할 경우에는 pairs() 를 사용해야 한다.

    t = { ["a"] = 1, ["b"] = 2, ["c"] = 3 }
    for i,v in ipairs(t) do print(i,v) end

    tt = {[1]=1,[3]=3,[5]=5}
    for i,v in ipairs(tt) do print(i,v) end
    1 1
    ttt = {[1]=1,[2]=3,[3]=3,[5]=5}
    for i,v in ipairs(ttt) do print(i,v) end
    1 1
    2 3
    3 3
    tttt = {"a","b","c"}
    for i,v in ipairs(tttt) do print(i,v) end
    1 a
    2 b
    3 c

    luabind : param 개수 제한

    C++ 메써드를 바인딩할 때, 파라미터의 개수가 제한되어 있는 듯하다. 좀더 테스트가 필요할 듯.

    luabind : C++에서 루아 테이블 객체 생성해서 functor 로 넘기는 법

    C++에서 루아 함수를 호출할 때, 파라미터의 개수가 상황에 따라서 동적으로 바뀔 경우 루아 테이블에 넣어서 보내면 깔끔하다. 그런데, 어떻게 C++ 스코프에서 루아 테이블을 만들 수 있을까? 생각보다 꽤 간단했다.

    -- lua
    function f(a) return a.x+a.y end

    // C++
    luabind::object table = luabind::newtable(L);
    table["x"] = 1;
    table["y"] = 2;
    functor f(L,"f");
    f(table);

    테이블 안에 테이블을 넣는 것도 가능하다.

    // C++
    luabind::object table = luabind::newtable(L);
    luabind::object subtable = luabind::newtable(L);
    table["sub"] = subtable;
    functor f(L,"f");
    f(table);

    luabind : virtual member function

    class Connection {
    virtual int handle_input();
    virtual int handle_output();
    virtual int handle_msg();
    };
    class TestConnection : public Connection {
    virtual int handle_msg();
    };

    이와 같이, 하위 클래스에서는 몇 개의 virtual member function 만을 override 한 경우, 바인딩할 때에는 주의가 필요하다.

    // 컴파일 에러 발생
    module(L)
    [
    class("TestConnection")
    .def("handle_input",&TestConnection::handle_input)
    .def("handle_output",&TestConnection::handle_output)
    .def("handle_msg",&TestConnection::handle_msg)
    ];

    // 컴파일 성공
    module(L)
    [
    class("Connection")
    .def("handle_input",&Connection::handle_input)
    .def("handle_output",&Connection::handle_output)
    .def("handle_msg",&Connection::handle_msg)
    ,
    class("TestConnection")
    .def("handle_msg",&TestConnection::handle_msg)
    ];

    단 virtual 이 잘 동작하는지는 확인 요망...

    luabind : functor

    루아 함수를 C++ 에서 호출할 수 있게 해준다. 템플릿으로 구현되어 있어서 컴파일 속도가 장난 아니게 느리다.

    -- lua
    function f(a,b) return a+b end

    // C++
    functor f(L,"f");
    f(1,2);

    어떤 루아 함수를 사용하려 한다는 것은, 이미 루아 스택에 그 함수가 로딩되어 있어야 한다는 의미이다. 그런데, 만약 런타임에 함수를 정의하고 그 함수를 C++ 에서 사용하려면 어떻게 해야 할까? 이를 위해서는 functor 등록 함수를 luabind 를 이용해서 다시 루아에서 호출해줘야 한다. 즉

    -- lua
    function f(a,b) return a+b end
    register_f()

    // C++
    void register_f()
    {
    functor * pFunc = new functor(L,"f");
    }

    module(L)
    [
    .def("register_f",&register_f)
    ];

    까다로운 점은 register_f() 에서의 루아스택을 어떻게 알아내느냐인데, [레이옷]은 SingletonPattern 으로 해결해버렸다. -_-;;

    끝으로 주의사항: functor 의 파라미터로 C++ 클래스를 사용하려면 필히 먼저 이 클래스를 luabind 로 등록해두어야 한다. (물론 루아에서 사용하는 메써드만 달랑 해주면 끝)

    luabind : __tostring()

    luabind 로 C++ 클래스 인스턴스를 생성, print(x) 를 해보고 싶다면, ostream operator << 를 implement 하고, tostring(self)를 바인딩해주면 된다.

    class number {};

    std::ostream& operator<<(ostream&, number&);

    module(L)
    [
    class_("number")
    .def(tostring(self))
    ];

    반면 루아 클래스 객체를 출력하려면 다음과 같이 함수를 정의해야 한다. '''이때 self 파라미터를 수동으로 넘긴다는 점에 유의할 것'''. (다른 함수에서 self 는 자동적으로 인스턴스 자신을 가리키지만, metatable 관련 함수들은 수동으로 넘겨야 하는 듯. 당연히 굳이 self 라는 이름일 필요는 없다)

    class 'XXX'
    function XXX:__init()
    self.v = 1
    end
    function XXX:__tostring(self)
    return 'XXX('..self.v..')'
    end

    lua list

    table 을 list 처럼 사용할 수 있다.

    -- create
    list = nil
    for line in io.lines() do
    list = { next=list, value=line }
    end

    -- iterate
    l = list
    while l do
    print(l.value)
    l = l.next
    end

    see also:

    • [http://www.lua.org 루아 공식 홈페이지]
    • [http://www.redwiki.net/wiki/wiki.php/Lua 레드픽셀님의 루아 페이지]
    • [http://lua-users.org/wiki/] - 루아 위키
    • [http://www.workspacewhiz.com/LuaPlus/index.html LuaPlus] - 가장 OO 스러운 루아 확장 라이브러리
    • [http://sourceforge.net/projects/lua-users/ lua-users.org at SF] - 윈도우용 루아 바이너리 및 매뉴얼 등등을 배포하는 곳.
    • [http://www.sjbrown.co.uk/lualite.html LuaLite] - Lua Syntax Highlight plugin for VS.NET. 단, 이것을 띄워 놓은 상태에서 검색을 하면 먹통이 되므로 조심할 것
    • [http://luaforge.net/ LuaForge]
    • [http://www.icynorth.com/forums/index.php LuaForum]
    • http://www.thomasandamy.com/projects/CPB/
  • 2006-06-04

    윈도우즈에서 IP 보안 정책 설정하기

    XP 부터 소프트웨어 방화벽을 지원하지만, TCP/UDP에 대해 특정 포트를 On/Off 시키는 정도의 제어만 가능하다. 그러나 Windows 서버 2003 부터는 - 리눅스에서는 오래전부터 지원했던 - IpSec 을 지원한다. 이는 관리도구 - 로컬보안정책 - IP보안정책 에서 설정할 수 있다.

    그러나 가장 좋은 것은 뭐니뭐니해도 하드웨어 방화벽이다. :)

    보안 정책

    보안 정책은 {IP 필터:필터 동작}의 매핑으로 이루어진다.

    • IP 필터 - {src_ip,dest_ip,protocol,port,...} 으로 구성된다.
    • 필터 동작 - 위 IP 필터에 의해 걸러진 트래픽을 허용/보안요청/보안필요/거부할 것인지를 결정한다.

    기본적으로 3가지의 보안 정책이 존재한다. 단 OS 설치 초기에는 모든 정책이 꺼져 있다.

    • 서버(보안요청)
    • 클라이언트(응답만)
    • 보안 서버(보안필요)

    따라서, 우리는 새로운 커스텀 보안정책을 생성한 후 아래와 같이 필터와 동작들을 추가해야 한다.

    • 터미널 서비스 : 회사의 개발용 머신, IDC의 다른 컴퓨터들에서만 허용
    • MSSQL : IDC의 다른 컴퓨터에서만 허용
    • WWW : 방화벽 없슴

    IP필터

    터미널 서비스

    터미널 서비스는 TCP 3389 포트를 사용한다. 따라서, 기본적으로 터미널 서비스를 거부한 다음, 접근을 허용하고 싶은 컴퓨터의 IP만 지정해주면 간단하다. OS 방화벽을 함께 사용한 경우, TCP 3389 포트를 열어주는 것을 잊지 말자.

    반면, 특정 subnet에서만 터미널 서비스를 허용하려면 , 해당 subnet 이 시작하는 주소를 잘 지정해야 한다. 하긴 mask 만 잘 지정하면 적당한 대역의 IP를 넣으면 알아서 변환이 되니깐 별 관계는 없다만...

    정책 프로토콜 원본주소 원본포트 대상주소 대상포트 필터동작
    모든 터미널 서비스 거부 TCP any any 서버의 IP 3389 거부
    특정 컴퓨터의 터미널 서비스 허용 TCP 특정 컴퓨터 - IP 또는 DNS any 서버의 IP 3389 허용
    특정 서브넷의 터미널 서비스 허용 TCP 특정 subnet- 시작 IP / subnet mask any 서버의 IP 3389 허용

    SqlServer 에 대한 외부 접근 제어

    SqlServer 는 TCP 1433, UDP 1433 포트를 사용한다. OS 방화벽을 함께 사용한 경우, 각 포트들을 열어주는 것을 잊지 말자. TCP/UDP에 대한 거부 필터를 2개 만들어서 추가한 뒤, 접근을 허용하고 싶은 서버를 허용 필터로 추가하면 된다. (보안의 기본적인 컨셉은 모두 막은 뒤 하나씩 풀어주는 것..이라고 누가 말했던가..)

    정책 프로토콜 원본주소 원본포트 대상주소 대상포트 필터동작
    SqlServer 외부 접근 거부 TCP/UDP 각각 any any 서버의 IP 1433 거부
    SqlServer 특정 서버 허용 TCP/UDP 각각 특정 서버의 IP 또는 DNS any 서버의 IP 1433 허용
  • 2006-06-04

    OLE DB Tips

    multiple result set + parameter binding

    • 캐릭터 정보 로딩과 같이, 하나의 프로시저에서 여러 테이블의 row 들을 select 하면서 파라미터도 바인딩해야 할 경우, static binding 으로는 구현할 수 없으며, CDynamicParameterAccessor::GetParam()를 이용해야 한다.
    • 파라미터의 경우, select 해온 모든 result set 을 소진한 후, 가장 마지막에 읽어와야 한다. 즉 DB_S_NORESULT == GetNextResult(...); 를 실행한 후에만 유효하다는 뜻이다. 만약, 그 전에 읽게 되면 garbage 가 리턴된다.
    • Bind()와 무관하므로, GetInterface()!=NULL 체크를 할 필요가 없다.

    CCommand::Prepare()

    한번 생성후 재사용하는 구조가 필요하다.

    GetNextResult(rowCount)

    웬지 계속 -1 이 넘어오는 걸로 봐서 뭔가 문제가 있는 듯? property 설정이 필요한가?

    DateTime to String

    • VarBstrFromDate 이걸 써야 될까나~
    • use
    HRESULT DataConvert (
        DBTYPE          wSrcType,
        DBTYPE          wDstType,
        DBLENGTH        cbSrcLength,
        DBLENGTH *      pcbDstLength,
        void *          pSrc,
        void *          pDst,
        DBLENGTH        cbDstMaxLength,
        DBSTATUS        dbsSrcStatus,
        DBSTATUS *      pdbsStatus,
        BYTE            bPrecision,
        BYTE            bScale,
        DBDATACONVERT   dwFlags);
    

    Connection Pooling

    • SqlServer Provider 를 기반으로, ThreadPooling 을 이용해 쿼리를 할 경우, DataSource 가 1개이고 Session 이 N 개를 생성했다면, 실제 DB에 대한 연결은 N 개가 된다.
    • 즉, Session 이 내부적으로 연결 풀링을 사용하게 되므로, DataSource 객체는 1개만 만들어도 된다.

    어떤 Accessor 를 사용할 것인가?

    MSDN 의 비교표를 간략하게 요약하면 다음과 같다.

    • CAccessor : 매크로를 이용한 컴파일 타임 바인딩이므로 성능 면에서 가장 우수하지만, 쉽게 말해서 하드코딩이므로 귀찮은 작업들이 많다. 컬럼 및 파라미터 바인딩 모두 지원.
    • CDynamicAccessor : 런타임 바인딩. 따라서 컬럼 정보를 읽어와야한다. 대신 파라미터 바인딩은 불가능. 뭔가 컴파일 타임에 정보를 알기 힘든 경우, 또는 multiple result set 을 리턴하는 프로시저에 사용하면 된다.
    • CDynamicParameterAccessor : CDynamicAccessor 에 파라미터 바인딩 기능이 추가되었다. 당연히 더욱 느려졌다.
    • CDynamicStringAccessor : CDynamicAccessor 와 유사하지만 컬럼값을 스트링으로 리턴한다.
    • CManualAccessor : AddBindEntry(), AddParameterEntry() 를 이용해서 수동으로 컬럼과 파라미터를 바인딩한다. 아마 CAccessor 에 이어 두번째로 빠른 듯.
    • CXMLAccessor : CDynamicStringAccessor 와 같지만, XML 스트링의 형태로 컬럼값을 리턴한다.

    성능적인 순서로 정렬해 본다면 대충 다음과 같지 않을까? (이건 본인의 추측)

    • CAccessor < CManualAccessor < CDynamicAccessor < CDynamicStringAccessor < CXMLAccessor < CDynamicParameterAccessor

    단, 파라미터 바인딩을 지원하는 넘은 아래의 3가지 뿐이다.

    • CAccessor
    • CManualAccessor
    • CDynamicParameterAccessor

    see also

  • 2006-06-01

    STL Tips

    dll + STL

    dll 에서 hash_set 을 멤버로 가진 클래스를 만들려다가 nested class 인 _Hash 라든지 hash_compare 등등을 모두 dllexport 하라길래 포기하고 PIMPL 패턴으로 해결해야만 했다.

    boost::lambda

    아래의 mem_fun_if 류와 비슷한 역할을 하는 것이 바로 boost::lambda 라이브러리다. 일단 아래처럼 포인터 컨테이너를 순회하면서 NOT NULL 일 경우에 특정 멤버 함수를 호출하는 예제를 살펴보자.

    for_each(l.begin(), l.end(), if_then(_1 != constant((A*)NULL), bind(&A::print, _1)));
    

    lambda 라이브러리를 사용하면 위와 같이 간단한 코드로 표현이 가능하다. 대신 컴파일 속도는... ㅠㅠ

    mem_fun_if

    일반적으로 포인터 컨테이너를 순회하면서 특정 조건을 만족할 경우 멤버 함수를 호출하는 패턴을 위한 자작 adapter function 이다. (물론 const 버전과 파라미터 1개를 받는 버전, 합쳐서 총 4개의 클래스와 4개의 함수를 만들어야 하지만... 시간상 생략)

    template<class _Pred, class _Result, class _Ty>
    class mem_fun_if_t : public unary_function<_Ty *, _Result>
    {
    public:
      explicit mem_fun_if_t( _Pred pred, _Result (_Ty::*_Pm)())
      : _pred(pred), _Pmemfun(_Pm), _count(0)
      {
      }
    
      // 단점이라면 멤버함수의 리턴값은... 도무지 캐치불가능...
      bool operator()(_Ty *_Pleft) const
      {
        if ( _pred(_Pleft) )
        {
          ((_Pleft->*_Pmemfun)());
          _count++;
          return true;
        }
        return false;
      }
    
      int count() const { return _count; }
    
    private:
      _Pred _pred;  // predicate
      _Result (_Ty::*_Pmemfun)(); // the member function pointer
      mutable int _count;
    };
    
    template<class _Pred, class _Result, class _Ty>
    inline
    mem_fun_if_t<_Pred,_Result, _Ty>
    mem_fun_if(_Pred pred,_Result (_Ty::*_Pm)())
    {
      return (mem_fun_if_t<_Pred,_Result, _Ty>(pred,_Pm));
    }
    

    이를 이용한 샘플이다.

    using namespace std;
    
    class A
    {
    public :
      explicit A( const string & s ): str(s) {}
      void print() { cout <<str <<endl; }
      string str;
    };
    
    #define not_null(T) bind2nd( not_equal_to<T*>(), (T*)0)
    
    int _tmain(int argc, _TCHAR* argv[])
    {
      list<A*> l;
      l.push_back( new A("Hello") );
      l.push_back( NULL );
      l.push_back( new A("World") );
      l.push_back( NULL );
      l.push_back( new A("!") );
    
      for_each( l.begin(), l.end(), mem_fun_if( bind2nd( not_equal_to<A*>(), (A*)0 ), &A::print ) );
      for_each( l.begin(), l.end(), mem_fun_if( not_null(A), &A::print ) );
    
      return 0;
    }
    

    if_not_null

    다음은 포인터 컨테이너에서 NOT NULL 인 갯수를 찾는 코드이다.

    count_if( l.begin(), l.end(),
    bind2nd( not_equal_to<A*>(), (A*)0 ) );
    

    ptr_fun and not1, not2

    ptr_fun 은 말 그대로 pointer to function 이다.

    template<class Arg, class Result>
    pointer_to_unary_function<Arg, Result>
    ptr_fun(Result (_*pfunc)(Arg));
    
    template<class Arg1, class Arg2, class Result>
    pointer_to_binary_function<Arg1, Arg2, Result>
    ptr_fun(Result (_*pfunc)(Arg1, Arg2));
    

    not1 은 unary_function 의 negate 이며, not2 는 binary_function 의 negate 이다.

    template<class UnaryPredicate>
    unary_negate<UnaryPredicate> not1(
      const UnaryPredicate& _Pred
    );
    
    template<class Predicate>
    class unary_negate
    : public unary_function<
      typename Predicate::argument_type,
      bool>
    {
     public:
      explicit unary_negate(const Predicate& _Func);
      bool operator()(const typename Predicate::argument_type& _Left) const;
    };
    
    template<class BinaryPredicate>
    binary_negate<BinaryPredicate> not2(
      const BinaryPredicate& _Func
    );
    
    template<class Operation>
    class binary_negate
    : public binary_function <
      typename Operation::first_argument_type,
      typename Operation::second_argument_type,
      bool>
    {
      public:
      explicit binary_negate(
        const Operation& _Func
      );
      bool operator()(
        const typename Operation::first_argument_type& _Left,
        const typename Operation::second_argument_type& _Right
      ) const;
    };
    

    그럼 그동안 배운 것을 토대로 다음 MSDN 샘플을 살펴보자.

    not1( bind2nd( ptr_fun(strcmp), "pearly" ) )
    

    만약 1초만에 strcmp(X,"pearly")==0 임을 인지해 냈다면 당신은 STL의 고수... ㅠㅠ

    mem_fun vs. mem_fun_ref

    EffectiveSTL 에도 나오는 이야기이니, 책을 가지신 분은 43 아이템을 참고하라.

    나를 비롯한 대부분의 프로그래머들은 이런 코드를 사랑한다.

    for ( list<GameObject*>::iterator itr = gameObjects.begin() ; itr != gameObjects.end() ; itr ++ )
    {
      GameObject * pObject = *itr;
      pObject->Update();
    }
    

    너무나도 직관적인 코드이지만, 사실 mem_fun 을 사용하면 한 줄로 해결이 가능하다.

    for_each( gameObjects.begin(), gameObject.end(), mem_fun(&GameObject::Update) );
    

    코드는 간단해졌지만 익숙하지 않은 관계로 팀원들이 싫어할 지도 모른다. 그러나, 타이핑하는 글자수도 적으면서 성능도 훨씬 더 좋다는데... 그 누가 안 쓸수 있으랴? (대신 디버깅할 때에는 멤버함수에다가 breakpoint 를 걸어야 하는 단점이 있을 수도 있다 ;) )

    포인터의 컨테이너일 경우에는 mem_fun 을, 객체의 컨테이너라면 mem_fun_ref 를 사용하면 된다. 만약 멤버함수에 파라미터를 넘겨야 한다면, 아래와 같이 bind 함수를 사용할 것.

    for_each( gameObjects.begin(), gameObject.end(), bind2nd( mem_fun(&GameObject::SetHP),100) );
    

    bind1st vs. bind2nd

    우선 bind 함수를 알아보기에 앞서, unary_function 과 binary_function 에 대해서 알아보자. 이들은 단지 파라미터와 리턴타입에 대한 typedef 만을 담는 template structure 일 뿐이다. 즉 이들은 혼자서는 아무런 기능도 하지 않지만, 이를 상속받은 하위 클래스에서 타입 정보를 적절히 참조하기 좋도록 디자인된 베이스 클래스라는 뜻이다.

    template<class Arg, class Result>
    struct unary_function {
      typedef Arg argument_type;
      typedef Result result_type;
    };
    
    template<class Arg1, class Arg2, class Result>
    struct binary_function {
      typedef Arg1 first_argument_type;
      typedef Arg2 second_argument_type;
      typedef Result result_type;
    };
    

    기본적으로 STL 알고리즘에서 사용되는 Predicate 등의 함수들은 unary_function 의 형태를 띄어야 한다. 쉽게 말하면, find(), for_each(), count() 의 마지막 인자가 해당 컨테이너 내부의 객체 하나만을 인자로 받는 함수 객체라는 뜻이다.

    가령 list 에서 10보다 큰 객체를 찾으려면 대략 다음과 같은 predicate 을 작성해야 한다.

    class GreaterThan10
    {
    public :
      bool operator() ( int i ) { return i>= 10; }
    };
    

    사실 이렇게 짜면 너무 코드가 범용적이지 못하므로, 대부분의 경우에는 이렇게 구현할 것이다.

    class GreaterThanN
    {
    public :
      GreaterThenN( int N ) : m_N(N) {}
      bool operator() ( int i ) { return i>= m_N; }
    private :
      int m_N;
    };
    

    이걸 STL 적인 생각으로 다시 구현한 것이 바로 bind 함수들이다. bindN(A,B) 은 binary_function A 의 N 번째 파라미터로 parameter B 을 바인딩해서, unary_function 을 만들어주는 놈이다. 얘네들을 잘 활용하면 단순 함수들은 간단히 한줄로 구현할 수 있게 된다.

    • bind1st( BINARY_FUNC, PARAM ) : PARAM 을 BINARY_FUNC의 1st 파라미터로 바인딩한다. bind1st( greater, 10 ) --> 10 > N
    • bind2nd( BINARY_FUNC, PARAM ) : PARAM 을 BINARY_FUNC의 2nd 파라미터로 바인딩한다. bind2nd( greater, 10 ) --> N > 10

    bind1st( greater, 10 ) 이 어떻게 10 > N 이라는 함수로 바뀌는지 자세히 살펴본다면...

    1. greater는 binary_function 이다.
    template<class Type>
    struct greater : public binary_function <Type, Type, bool>
    {
      bool operator()(
        const Type& _Left,
        const Type& _Right
      ) const;
    };
    
    1. bind1st(A,B) 는 binder1st 객체를 리턴한다.
    template<class Operation, class Type>
    binder1st <Operation> bind1st(
      const Operation& _Func,
      const Type& _Left
    );
    
    1. binder1st<A> 는 A의 두번째 파라미터를 파라미터로 받고, A의 리턴값을 리턴하는 함수 객체이다. 이때 이 클래스의 생성자의 인자 목록을 유심히 살펴보면, binary_function 하나와 이놈의 하나의 파라미터는 받고 있다. (나머지 하나는 당연히 container 에서 받게 된다.)
    template<class Operation>
    class binder1st
    : public unary_function <
    typename Operation::second_argument_type,
    typename Operation::result_type>
    {
      public:
        typedef typename Operation::second_argument_type argument_type;
        typedef typename Operation::result_type result_type;
        binder1st(
          const Operation & _Func,
          const typename Operation::first_argument_type& _Left
        );
        result_type operator()(
          const argument_type& _Right
        );
        result_type operator()(
          const argument_type& _Right
        ) const;
      protected:
        Operation op;
        typename Operation::first_argument_type value;
    };
    

    SafeDeleter

    템플릿 클래스 버전을 사용할 경우, 매번마다 타입을 지정해줘야 하지만, 템플릿 멤버함수 버전을 사용하면 타입 없이도 사용할 수 있다. 아직까지 이걸 몰랐다니.. ㅠㅠ 쑥갓군 땡큐~

    /// template class version
    template <class T>
    class SafeDeleter
    {
      public :
      void operator () ( T * & ptr ) const
      {
        SAFE_DELETE(ptr);
      }
    };
    
    for_each( a.begin(), a.end(), SafeDeleter<A>() );
    
    /// template member function version
    class SafeDeleter2
    {
      public :
      template <class T>
      void operator () ( T * & ptr ) const
      {
        SAFE_DELETE(ptr);
      }
    };
    
    for_each( a.begin(), a.end(), SafeDeleter2() );
    

    Static Key vs. Dynamic Key

    class Object
    {
      KeyType_t Key;
      ....;
    };
    map<KeyType_t,Obj*> orderedObjects;
    

    위와 같이 객체의 특정 필드의 값으로 정렬된 map이 있다고 하자. 일단 map에 객체를 넣고 난 다음에 Key 값을 변경하면 map 내부의 트리가 깨지므로 바꿔서는 곤란해진다. 그러나, 때로는 Key 값이 바뀌어야 할 필요가 있다. 언제나 실시간으로 바꿔주려면 map 에서 객체를 빼낸 다음 Key 값을 바꾸고 다시 넣어야 하는데, 항상 넣었다 뺐다 하기에는 성능적인 부분이 걱정되는데...

    이럴 경우에는 외부에서 필요할 때마다 새로운 키를 저장한 다음, 적당히 바꿀 타이밍이 되면 객체를 map 에서 빼낸 다음 키값을 업데이트해주면 된다. 특히 아래와 같이, 정적인 메인 키의 맵으로 구성된 객체의 매니저가 내부에 특정 순서로 정렬된 서브 키의 맵을 가진 경우, 위와 같은 패턴을 사용하면 꽤 편하다.

    class Object
    {
      StaticKeyType_t MainKey;
      DynamicKeyType_t SubKey;
      DynamicKeyType_t NewSubKey;
      ....;
    };
    
    class ObjectManager : public map<StaticKeyType_t,Object*>
    {
    map<DynamicKeyType_t,Object*> orderedObjects;
    void ChangeSubKey( StaticKeyType_t mainKey, DynamicKeyType_t newSubKey );
    void UpdateSubMap();
    };
    

    어쨌든, map 의 키값을 외부에서 바꿨다가는 큰 낭패를 볼 것이다.

    see also:

    multixxx::erase(key_type)

    일반적인 erase()는 iterator/const_iterator 를 파라미터로 받아서 그 넘을 지우는 역할을 한다. 그런데, multimap/multiset 에서는 key_type 만 넘겨서 해당 값을 지우는 메쏘드를 제공한다. 대체로 이는 잘 동작하지만 predicate 가 명시된 multimap/multiset 에서는 생각대로 동작하지 않는다. 다음 예를 보자.

    class TeamInfo
    {
    public :
      TeamInfo ( int point = 0 )  : TeamPoint(point) {}
      int TeamPoint;
    };
    
    class GreaterTeam
    {
    public :
      bool operator () (const TeamInfo* l, const TeamInfo* r) const
      {
        return l->TeamPoint> r->TeamPoint;
      }
    };
    
    void
    test_multiset()
    {
      typedef multiset<TeamInfo*,GreaterTeam> TEAMS;
    
      TEAMS teams;
    
      TeamInfo t1(1);
      TeamInfo t2(1);
      TeamInfo t3(0);
      TeamInfo t4(2);
    
      teams.insert( &t1 );
      teams.insert( &t2 );
      teams.insert( &t3 );
      teams.insert( &t4 );
    
      // erase by key test
      teams.erase( &t4 ); assert( teams.size() == 3 );
      teams.erase( &t1 ); assert( teams.size() == 1 );  // t1 과 t2 를 지운다. 낭패~
    
      // erase with iterator test
      teams.clear();
      teams.insert( &t1 );
      teams.insert( &t2 );
      teams.insert( &t3 );
      teams.insert( &t4 );
    
      pair<TEAMS::iterator,TEAMS::iterator> itrs = teams.equal_range(&t1);
      teams.erase( find( itrs.first, itrs.second, &t1 ) );
      assert( teams.size() == 3 );
    
      assert( *(teams.find(&t1)) == &t2 ); // t1 을 찾으면 t2 가 나온다는 놀라운 사실!
      assert( *(teams.find(&t2)) == &t2 );
      assert( *(teams.find(&t3)) == &t3 );
      assert( *(teams.find(&t4)) == &t4 );
    }
    

    여기서 배울 수 있는 교훈은

    • 코드상으로는 equal_range(&t1)이라는 것이 t1을 찾는 것처럼 보이지만, 실제로는 key_type과 무관하며 오직 predicate 를 이용해서 검색을 한다!
    • multi 시리즈에서는 find()나 erase()에 있어서 항상 동일한 값이 존재할 수 있다!
    • predicate 의 비교문에서 >= 을 사용하면 다른 결과가 나온다!
    • predicate 관련 변수(m_TeamPoint)와 같이, 비교 로직에 영향을 주는 무언가를 외부에서 바꾸게 되면 multixxx 내부의 트리가 깨지게 된다. 너무나 당연한 사실이지만, Key 를 바꾸는게 아니라서 안전할 거 같아 보이는 것도 사실이다. 이를 방지하려면 일단 container 에서 해당 값을 erase한 다음 값을 수정하고 다시 insert 하는 것을 권장한다.