C++ of the Day #30 - Python의 map 함수 따라 잡기

Python의 built-in 함수중에 map이라는 것이 있습니다. 특정 sequence의 내용을 제공된 함수를 통해 처리하여 다시 sequence로 출력하는 함수입니다. 다음의 예를 보면 쉽게 사용법을 알 수 있습니다.
>>> map(lambda x: x * 2, range(0, 11))
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
이 함수의 특징은 다음과 같이 여러개의 sequence를 입력으로 받아 처리할 수 있다는 점입니다. 물론 이때 제공되는 함수는 입력되는 sequence만큼의 인자를 받을 수 있어야 합니다.
>>> map(lambda x, y: x + y, range(0, 11), range(0, 11))
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

>>> map(lambda x, y, z: x + y + z, range(0, 11), range(0, 11), range(0, 11))
[0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30]

>>> map(lambda x, y: x + y, range(0, 11), range(0, 11), range(0, 11))
Traceback (most recent call last):
File "<pyshell#5>", line 1, in -toplevel-
map(lambda x, y: x + y, range(0, 11), range(0, 11), range(0, 11))
TypeError: <lambda>() takes exactly 2 arguments (3 given)
이번 글에서는 C++을 사용하여 위의 기능을 만들어 보겠습니다.

만들기 전에 먼저... C++에서 제공하는 algorithm 중에 transform이라는 함수가 위의 map과 동일한 기능을 합니다. std::transform의 문제라면 최대 두개까지의 input sequence밖에는 받을 수 없다는 점이죠. 사실 이는 tr1 이전의 std 라이브러리의 문제점이기도 합니다. N개의 뭔가가 필요한 부분이 나오면 std 라이브러리에서는 N을 두개로 구현해 둔 것이죠. std::pair가 그렇고 binder1st, binder2nd 가 그렇습니다. transform도 그중의 하나지요.((tr1으로 오면서 tr1::tuple이나 binder 등에서는 이러한 제한이 없어졌습니다. 물론 N 값의 한계는 존재합니다만 boost에서는 이를 컴파일시에 변경할 수 있도록 해 놓고 있습니다.))

그럼 잠깐 이 transform의 사용법을 살펴 보겠습니다. Python의 map과 달리 sequence 자체 대신 iterator pair가 사용됩니다. stl 전체에서 볼 수 있는 패턴이죠.
transform(in1.begin(), in1.end(), out.begin(), _1*2);
transform(in1.begin(), in1.end(), in2.begin(), out.begin(), _1*_2);
// transform(in1.begin(), in1.end(), in2.begin(), in3.begin(), out.begin(), _1*_2*_3);
// compile error
우리가 작성할 함수는 이 transform의 구조와 동일하며 tf라는 이름으로 만들어 보도록 하겠습니다. map은 이미 container의 이름으로 사용되고 있기 때문이죠. :-) 물론 tf 함수는 input iterator의 갯수에 제한이 없도록 만들도록 합니다. 먼저 하나와 두개의 iterator 를 받는 tf 함수는 다음과 같습니다. 그리고 transform과 동일하게 사용이 가능합니다.
template <class Func, class InIter, class OutIter>
void tf(InIter first, InIter last, OutIter out, Func func) {
  for (;first != last; ++first) {
    *out++ = func(*first);
  }
}

template <class Func, class InIter, class InIter0, class OutIter>
void tf(InIter first, InIter last, InIter0 first0, OutIter out, Func func) {
  for (;first != last; ++first , ++first0) {
    *out++ = func(*first, *first0);
  }
}
여기까진 어려움이 없습니다. 문제는 다음부턴데요 하나하나 이렇게 손으로 작성하자니 좀 짜증이 난다는 것이죠. 한 10개 정도까지는 신경써서 손으로 작성할 수도 있겠지만 input iterator를 한 200개 정도 받게 하려면 도저히 손으로 할 수 있는 범위가 아닙니다.

여기서 등장해야 하는 것이 바로 Boost.Preprocessor 라이브러리입니다. 위의 두 tf 함수를 다음과 같이 비교해 보면 어떤 부분이 N 값에 따라 추가되어야 할지를 알 수 있습니다.
template <class Func, class InIter/*, class InIter0*/, class OutIter>
void tf(InIter first, InIter last/*, InIter0 first0*/, OutIter out, Func func) {
  for (;first != last; ++first /*, ++first0*/) {
    *out++ = func(*first/*, *first0*/);
  }
}
먼저 ", class InIter0" 라는 부분을 살펴 보겠습니다. N이 0이라면 이 부분은 없어야 합니다. N이 1이라면 ", class InIter0"가 되고 N이 2라면 ", class InIter0, class InIter1", 이런 식으로 N값에 따라 하나씩 늘어나야 하는 부분입니다. 이를 위해서는 Boost.Preprocessor 라이브러리에서 제공하는 BOOST_PP_ENUM_TRAILING_PARAMS(n, class InIter)를 사용하면 됩니다. 여기서 TRAILING은 n이 0이 아닌 경우 앞에 comma를 붙여주는 함수라는 것을 나타냅니다.

두번째 부분인 ", InIter0 first0" 부분은 문자열의 두군데의 숫자가 N값에 따라 정해져야 합니다. 이를 위해서는 BOOST_PP_ENUM_TRAILING_BINARY_PARAMS(n, InIter, first)를 사용합니다. 이름에 있는 BINARY에서 알 수 있듯이 두개의 입력을 받아 각각 N값을 붙여 줍니다. 세번째와 네번째 경우는 첫번째 경우와 동일하게 각각 BOOST_PP_ENUM_TRAILING_PARAMS(n, ++first), BOOST_PP_ENUM_TRAILING_PARAMS(n, *first)를 사용하면 됩니다. 그럼 완성된 tr 함수를 보실까요?
#ifndef TF_MAX_SIZE
#  define TF_MAX_SIZE 3  // default maximum size is 3
#endif

#define TF_impl(z, n, unused)                                                
template <                                                                   
  class Func,                                                                
  class InIter BOOST_PP_ENUM_TRAILING_PARAMS(n, class InIter),               
  class OutIter                                                              
>                                                                            
void tf(                                                                     
  InIter first,                                                              
  InIter last BOOST_PP_ENUM_TRAILING_BINARY_PARAMS(n, InIter, first),        
  OutIter out,                                                               
  Func func)                                                                 
{                                                                            
  for (;first != last; ++first BOOST_PP_ENUM_TRAILING_PARAMS(n, ++first) ) { 
    *out++ = func(*first BOOST_PP_ENUM_TRAILING_PARAMS(n, *first) );         
  }                                                                          
}

BOOST_PP_REPEAT(TF_MAX_SIZE, TF_impl, ~)

#undef TF_impl
((#define TF_impl부터 라인의 끝마다 라인을 연결해주는 backslash가 있어야 하나 WP가 모두 지워버렸군요. :-| backslash가 있다고 생각하면서 보세요. 실제 코드는 다운로드받아 보실 수 있습니다.))

여기서는 위에서 만든 tf를 TF_impl이라는 이름으로 #define라고 이를 BOOST_PP_REPEAT를 사용하여 TF_MAX_SIZE까지 반복했습니다. 이를 preprocessing하면 다음과 같은 결과를 볼 수 있습니다.((g++ -E 옵션을 주면 preprocessing 결과를 볼 수 있습니다.))
template < class Func, class InIter , class OutIter > void tf( InIter first, InIter last , OutIter out, Func func) { for (;first != last; ++first ) { *out++ = func(*first ); } } template < class Func, class InIter , class InIter0, class OutIter > void tf( InIter first, InIter last , InIter0 first0, OutIter out, Func func) { for (;first != last; ++first , ++first0 ) { *out++ = func(*first , *first0 ); } } template < class Func, class InIter , class InIter0 , class InIter1, class OutIter > void tf( InIter first, InIter last , InIter0 first0 , InIter1 first1, OutIter out, Func func) { for (;first != last; ++first , ++first0 , ++first1 ) { *out++ = func(*first , *first0 , *first1 ); } }
#define을 사용하였기 때문에 전체 코드가 한줄로 확장된 것을 알 수 있습니다. 이를 horizontal repetition이라고 합니다. 물론 함수의 기능을 수행하는데 문제가 있는 것은 아니나 구현시 디버깅이 힘들어 집니다. 이를 좀 더 알아보기 쉽게 확장시키기 위해 vertical repetition중 file iteration을 사용해 보겠습니다. 이에 대한 자세한 설명은 생략하겠습니다. ;-)

아래의 코드는 원래 두 개의 파일로 작성할 것을 하나로 만들면서 좀 복잡해졌습니다만 실제 두개의 파일이라고 생각하고 나눠서 보면 쉽게 이해할 수 있습니다.
#ifndef BOOST_PP_IS_ITERATING

#  ifndef TF_HPP_INCLUDED
#    define TF_HPP_INCLUDED

#    include <boost/preprocessor/repetition.hpp>
#    include <boost/preprocessor/iteration/iterate.hpp>

#    ifndef TF_MAX_SIZE
#      define TF_MAX_SIZE 3  // default maximum size is 3
#    endif

// generate specializations
#    define BOOST_PP_ITERATION_LIMITS (0, TF_MAX_SIZE - 1)
#    define BOOST_PP_FILENAME_1       "tf.hpp" // this file
#    include BOOST_PP_ITERATE()

#  endif // TF_HPP_INCLUDED

#else // BOOST_PP_IS_ITERATING

#  define n BOOST_PP_ITERATION()

// specialization pattern
template <
  class Func,
  class InIter BOOST_PP_ENUM_TRAILING_PARAMS(n, class InIter),
  class OutIter
>
void tf(
  InIter first,
  InIter last BOOST_PP_ENUM_TRAILING_BINARY_PARAMS(n, InIter, first),
  OutIter out,
  Func func)
{
  for (;first != last; ++first BOOST_PP_ENUM_TRAILING_PARAMS(n, ++first) ) {
    *out++ = func(*first BOOST_PP_ENUM_TRAILING_PARAMS(n, *first) );
  }
}

#undef n

#endif // BOOST_PP_IS_ITERATING
이 파일의 이름을 tf.hpp로 저장하고 preprocessing이 시작되면 필요한 만큼 자기 자신을 다시 #include하면서 N에 해당하는 함수를 확장합니다. 제일 위의 BOOST_PP_IS_ITERATING 정의는 현재 이 파일이 최초 #include된 것인지 자신을 다시 #include한 것인지를 구분하는데 사용됩니다. 여기서 tf는 #define된 것이 아니므로 한줄이 아닌 파일에 작성한 형태 그대로 처리됩니다. N이 3일때의 출력 결과는 다음과 같습니다. 한결 보기가 좋아졌죠? 물론 N이 200일 경우에도 동작합니다. :-)
template <
  class Func,
  class InIter ,
  class OutIter
>
void tf(
  InIter first,
  InIter last ,
  OutIter out,
  Func func)
{
  for (;first != last; ++first ) {
    *out++ = func(*first );
  }
}
template <
  class Func,
  class InIter , class InIter0,
  class OutIter
  >
void tf(
  InIter first,
  InIter last , InIter0 first0,
  OutIter out,
  Func func)
{
  for (;first != last; ++first , ++first0 ) {
    *out++ = func(*first , *first0 );
  }
}
template <
  class Func,
  class InIter , class InIter0 , class InIter1,
  class OutIter
>
void tf(
  InIter first,
  InIter last , InIter0 first0 , InIter1 first1,
  OutIter out,
  Func func)
{
  for (;first != last; ++first , ++first0 , ++first1 ) {
    *out++ = func(*first , *first0 , *first1 );
  }
}
Python에는 인자를 넘기는 방법들이 다양하게 존재하며 이들을 활용하여 map과 같은 함수를 쉽게(?) 구현할 수 있습니다만 C++에서는 이런 방법들이 없습니다. 따라서 C++에서 이런 무한한(?) 인자를 제공하기 위해서는 실제로 무한한 인자를 가지는 함수들을 정의할 수 밖에 없습니다. 물론 손으로 해야 할때는 거의 불가능했으나 Boost.Preprocessor 라이브러리를 사용하면 비교적 쉽게 이를 구현할 수 있습니다. Boost의 mpl같은 라이브러리들도 이런 기능이 없었다면 구현하기가 무척 귀찮았겠죠? ;-)

Downloads

tf.zip

Comments

Popular Posts