C++이야기 스물여덟번째: map container에 STL algorithm적용하기

Posted at 2008. 11. 24. 07:00 // in S/W개발/C++ 이야기 // by 김윤수


If you are more familiar with English, then follow this link.
C++ Tips 28th: Applying STL Algorithms to a Map Container

여러분은 map container에 STL algorithm을 적용해 보신 적 있으신가요? map은 key=value pair를 저장해 두는 container라서 다른 container에 비해 STL algorithm을 적용하기가 쉽지 않더군요. map은 key 보다는 value에 관심이 있는 container라서 STL algorithm을 수행할 때도 key-value pair에 대해서 특정 작업을 수행하기 보다는 value에 대해서 어떤 작업을 수행하기를 원하는 경우가 많은데, STL algorithm 및 표준 함수 객체들이 map에 맞춰져 작성되어 있질 않아서 이걸 하기가 쉽지 않더군요. 그래서 별도의 Adaptor class를 정의하는 경우가 많습니다. 한 번 예를 들어 보겠습니다. 다음과 같은 클래스가 정의되어 있다고 가정해 보시죠.

class Elem {
public:
    Elem() : m_name(), m_val() {}
   
    Elem(const string& name, int val) : m_name(name), m_val(val) {}
   
    const string& GetName() const
    {
        return m_name;
    }
   
    int GetValue() const
    {
        return m_val;
    }
   
    void SetValue(int val)
    {
        m_val = val;
    }
   
    void Print() const
    {
        cout << GetName() << "'s value ==> " << GetValue() << endl;
    }
   
private:
    string m_name;
    int m_val;
};

이 Elem 를 담는 map을 다음과 같이 정의해서 몇 개를 집어 넣어 보도록 하겠습니다.

// 앞으로 설명에서 나오는 m container는 이걸 뜻합니다
map<string, Elem> m;
   
m["1"] = Elem("1", 1);
m["2"] = Elem("2", 2);
m["3"] = Elem("3", 3);
m["4"] = Elem("4", 4);
m["5"] = Elem("5", 5);

이 map에 담긴 모든 Elem 들의 값을 얻어내고 싶은 경우를 생각해 보겠습니다. 그렇다면 다음과 같이 for 루프를 작성할 수 있습니다.

// 먼저 값을 담을 vector를 정의합니다
vector<int> vi(m.size());
// 그리고 for 루프를 돌립니다
for (map<string, Elem>::iterator it = m.begin(); it != m.end(); ++it)
{
  vi.push_back(it->second.GetValue());
}

소위 STL 좀 쓴다하는 여러분이 위와 같은 코드를 그냥 둘리 만무합니다. 어떻게 하면 저 for 루프를 STL 알고리즘으로 바꿀 수 없을까를 고민하시겠지요. 그리고 transform을 쓰면 되겠구나라고 맘을 먹습니다.

// 먼저 algorithm header 파일을 포함해야죠
#include <algorithm>
#include <functional>
......

vector<int> vi(m.size());
// m 에 들어가 있는 것은 포인터가 아니라 값이 들어가 있으므로 mem_fun이 아닌
// mem_fun_ref를 사용하여 멤버 함수 객체를 만듭니다
transform(m.begin(), m.end(), vi.begin(), mem_fun_ref(&Elem::GetValue));

이 코드가 문제 없이 컴파일 돼서 실행될까요? 안타깝게도 그렇지 않습니다. transform이 기대하는 건 Elem 객체가 아닌 pair<string, Elem> 객체이기 때문에 컴파일 에러가 발생하지요. 여기서 잠깐 transform 알고리즘 속을 들여다 보면 왜 컴파일 에러가 발생하는지 좀 저 정확히 알 수 있습니다.

// 설명을 편하게 하기 위해 핵심 부분만 발췌했습니다.
template<typename _InputIterator, typename _OutputIterator,
                typename _UnaryOperation>
_OutputIterator
transform(_InputIterator __first, _InputIterator __last,
                _OutputIterator __result, _UnaryOperation __unary_op)
{
  for ( ; __first != __last; ++__first, ++__result)
    *__result = __unary_op(*__first);
  return __result;
}

파랗게 표시한 부분을 보시면 first iterator를 dereference하고 있습니다. first iterator를 dereference하면 당연히 Elem 객체가 아닌 pair<string, Elem> 객체가 되는 것이지요.

그렇다면 뭔가 Elem의 GetValue()를 호출해 줄 Adaptor가 필요하겠군요. 다음과 같이 Adaptor를 정의하면 될까요?

struct ElemGetValueAdaptor {
  int operator()(const pair<string, Elem>& p) {
    return p.second.GetValue();
  }
};

이런 클래스를 정의하고 나서 transform을 다음과 같이 호출하면 되겠습니다.

transform(m.begin(), m.end(), vi.begin(), ElemGetValueAdaptor());

뭐 여기까지는 할만합니다. 여기에 덧붙여 이번에는 m container에 있는 Elem 객체의 값을 출력해야 해서 Elem::Print()를 호출해야 하게 됐습니다. 그래서 Adaptor를 하나 더 정의합니다.

struct ElemPrintAdaptor {
  void operator()(const pair<string, Elem>& p) {
    p.second.Print();
  }
};

그리고 나서 for_each를 다음과 같이 호출하면 되겠지요.

for_each(m.begin(), m.end(), vi.begin(), ElemPrintAdaptor());

이렇게 매번 멤버 함수를 호출하려고 할 때마다 Adaptor를 정의하면 얼마 있지 않아 코드에 온갖 Adaptor들이 난무하게 될 것입니다. 그리고, Adaptor들의 모양새라는 것도 거의 반복되는 코드이구요. 반복되는 코드를 보다보면 당연히 어떻게 하면 반복되는 코드를 없앨까를 생각하게 되죠. 그래서 이걸 템플릿으로 할 수 없을까를 생각하시게 될 것입니다.

template <typename R>
struct ElemAdaptor {
    // GetValue()와 Print()가 모두 상수 멤버 함수라서 끝에 const를 붙임
    typedef R (Elem::*MemFunType)() const;
   
    ElemAdaptor(MemFunType mf) : m_mf(mf) {}
   
    R operator()(const pair<string, Elem>& p) {
        return (p.second.*m_mf)();
    }
   
private:
    MemFunType m_mf;
};

그리고, transform과 for_each는 다음과 같이 호출하면 되겠지요.

transform(m.begin(), m.end(), vi2.begin(), ElemAdaptor<int>(&Elem::GetValue));
for_each(m.begin(), m.end(), ElemAdaptor<void>(&Elem::Print));

이렇게 하면 GetValue()와 Print()는 커버할 수 있겠지만 SetValue(int)는 처리할 수 없습니다. Argument가 있기 때문입니다. 게다가 상수 멤버 함수도 아니구요. 이런 다양한 멤버함수를 처리할 수 있는 일반적인 멤버 함수 Adaptor를 만드는 건 보통 어려운 일이 아닙니다.
만약 m container가 Elem 객체의 container가 아니라 Elem* 의 container 거나 Elem&의 container이면 상황이 더 복잡해 질 것입니다. const나 volatile이 붙을 수도 있는 거구요. 이 모든 것들을 처리할 수 있는 일반적인 Adaptor를 만드는 건 무척이나 어려운 일이지요.

이럴 때 써먹을 수 있는 게 boost::bind 와 placeholder입니다. boost::bind는 앞에서 제가 언급한 다양한 차이들을 모두 극복하고 하나의 함수로 모든 것을 수용할 수 있습니다. 위에서 제가 직접 만들었던 Adaptor를 쓰지 않고 bind를 쓰는 방식으로 바꾼다면 다음과 같이 작성하시면 됩니다.

// boost::bind
#include <boost/bind.hpp>

using namespace std;
using namespace boost;
......
transform(
    m.begin(), m.end(), vi2.begin(),
    bind(&Elem::GetValue, bind(&map<string, Elem>::value_type::second, _1))
);
for_each(
    m.begin(), m.end(),
    bind(&Elem::Print, bind(&map<string, Elem>::value_type::second, _1))
);

inner bind() 즉 bind(&map<string, Elem>::value_type::second, _1) 이렇게 된 부분을 먼저 설명드리면, _1은 placeholder라고 하여 _1이라고 표시된 부분에 인자를 받을 수 있는 함수자(functor)를 만들어주게 됩니다. transform과 for_each는 각각 pair<string, Elem>를 입력인자로 하여 해당 알고리즘의 마지막 인자로 주어진 함수자를 호출할 것이므로 _1 placeholder를 써서 인자를 받을 수 있도록 한 것입니다. 그리고 inner bind의 첫번째 인자로 멤버 변수에 대한 포인터를 주게 되면, 두번째 인자로 주어진 객체(이 경우 _1)에 대해 멤버로 참조할 수 있도록 만들어 줍니다. &map<string, Elem>::value_type::second 라고 한 부분은 당연히 map 에서 Elem 객체를 참조할 수 있도록 하기 위한 것입니다. 이 설명을 종합하자면 inner bind는 pair<string, Elem> 객체에서 Elem 객체를 선택하는 부분이라고 생각하시면 되겠습니다. 즉, ElemAdaptor::operator(p) 의 구현에서 p.second 에 해당하는 부분이라고 생각하시면 되겠습니다.

이걸 바탕으로 outer bind를 좀 더 쉽게 표현해 보자면 bind(Elem_mem_func_ptr, Elem_obj), 이런 의미가 되겠습니다. bind는 첫번째 인자가 멤버 포인터(멤버 변수에 대한 포인터이건 멤버 함수에 대한 포인터이건 상관 없이)이면 두번째 인자를 객체(또는 객체 참조자 또는 객체 포인터)로 해석하고 해당 객체에 대해 멤버 변수나 멤버 함수를 접근 또는 호출해주는 함수자를 만들어 줍니다. 그래서 결국은 (Elem_obj.*Elem_mem_func_ptr)() 또는 (Elem_obj->*Elem_mem_func_ptr)() 의 효과가 있게 됩니다.

bind는 꼭 인자가 없는 멤버 함수에만 쓸 수 있는 건 아닙니다. 실은 인자를 9개까지 지원해 줍니다. 예를 들어, m container를 각 Elem 객체에 대해 Elem::SetValue(10) 를 호출하고 싶다면 다음과 같이 하시면 됩니다.

for_each(
  m.begin(), m.end(),
  bind(&Elem::SetValue, bind(&map<string, Elem>::value_type::second, _1), 10)
);

bind의 세번째 인자로 SetValue()의 인자값을 bind 하면 되는 것이지요.

정리하자면 map container에 STL algorithm을 적용하고 싶을 때는 boost::bind와 placeholder를 다음과 같은 패턴으로 사용하시면 되겠습니다.

#include <boost/bind.hpp>

using namespace boost;

bind(&ClassName::mem_func_name, bind(&map<keytype, 
        ClassName>::value_type::second, _1), arg1, arg2, ..., arg9)


그럼 여러분의 C++ 프로그래밍에 조금이라도 도움이 되길 바라면서 이만 줄여야겠네요.