Skip to main content

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 from this blog

1의 개수 세기 - 해답

벌써 어제 말한 내일이 되었는데 답을 주신 분이 아무도 없어서 좀 뻘쭘하네요. :-P 그리고 어제 문제에 O(1)이라고 적었는데 엄밀히 얘기하자면 O(log 10 n)이라고 적었어야 했네요. 죄송합니다. ... 문제를 잠시 생각해보면 1~n까지의 수들 중 1의 개수를 얻기 위해서는 해당 숫자 n의 각 자리의 1의 개수가 모두 몇개나 될지를 구해서 더하면 된다는 사실을 알 수 있습니다. 예를 들어 13이라는 수를 생각해 보면 1~13까지의 수에서 1의 자리에는 1이 모두 몇개나 되는지와 10의 자리에는 모두 몇개나 되는지를 구해 이 값을 더하면 됩니다. 먼저 1의 자리를 생각해 보면 1, 11의 두 개가 있으며 10의 자리의 경우, 10, 11, 12, 13의 네 개가 있습니다. 따라서 2+4=6이라는 값을 구할 수 있습니다. 이번엔 234라는 수에서 10의 자리를 예로 들어 살펴 보겠습니다. 1~234라는 수들 중 10의 자리에 1이 들어가는 수는 10, 11, ..., 19, 110, 111, ... 119, 210, 211, ..., 219들로 모두 30개가 있음을 알 수 있습니다. 이 규칙들을 보면 해당 자리수의 1의 개수를 구하는 공식을 만들 수 있습니다. 234의 10의 자리에 해당하는 1의 개수는 ((234/100)+1)*10이 됩니다. 여기서 +1은 해당 자리수의 수가 0이 아닌 경우에만 더해집니다. 예를 들어 204라면 ((204/100)+0)*10으로 30개가 아닌 20개가 됩니다. 이런 방식으로 234의 각 자리수의 1의 개수를 구하면 1의 자리에 해당하는 1의 개수는 ((234/10)+1)*1=24개가 되고 100의 자리에 해당하는 개수는 ((234/1000)+1)*100=100이 됩니다. 이들 세 수를 모두 합하면 24+30+100=154개가 됩니다. 한가지 추가로 생각해야 할 점은 제일 큰 자리의 수가 1인 경우 위의 공식이 아닌 다른 공식이 필요하다는 점입니다. 예를 들어 123에서 100의 자리에 해당하는 1의 개수는 ((123/1

std::map에 insert하기

얼마전 회사 동료가 refactoring한 코드를 열심히 revert하고 있어서 물어보니 다음과 같은 문제였습니다. 원래 코드와 refactoring한 코드는 다음과 같더군요. nvp[name] = value; // original code nvp.insert(make_pair(name, value)); // refactored 아시겠지만 위의 두 라인은 전혀 다른 기능을 하죠. C++03에 보면 각각 다음과 같이 설명되어 있습니다. 23.1.2/7 Associative containers a_uniq.insert(t): pair<iterator, bool> inserts t if and only if there is no element in the container with key equivalent to the key of t. The bool component of the returned pair indicates whether the insertion takes place and the iterator component of the pair points to the element with key equivalent to the key of t. 23.3.1.2/1 map element access [lib.map.access] T& operator[](const key_type& x); Returns: (*((insert(make_pair(x, T()))).first)).second. 원래 코드는 매번 새 값으로 이전 값을 overwrite했지만 새 코드는 이전에 키가 존재하면 새값으로 overwrite하지 않습니다. 따라서 원래 기능이 제대로 동작하지 않게 된것이죠. 그래서 물어봤죠. "왜 이렇게 했어?" "insert가 성능이 더 좋다 그래서 했지." :-? 사실 Fowler 아저씨는 Refactoring 책에서 refactoring은 성능을 optimizing하기 위한 것이 아니다라

C++ of the Day #9 - Boost.Python 사용하기 #1

Python 은 가장 인기있는 interpret 언어중의 하나입니다. Python의 장점 중 하나는 C/C++ 모듈과 쉽게 연동할 수 있다는 점입니다. 물론 손으로 일일히 wrapper를 만드는 것은 손이 많이 가고 에러를 만들수 있는 작업이나 SWIG 등과 같은 도구를 사용하면 쉽게 python 모듈을 만들 수 있습니다. Boost.Python 은 이런 SWIG와 같이 python 모듈을 쉽게 만들 수 있도록 도와주는 라이브러리로 순수 C++만을 사용한다는 점이 SWIG와 다른 점입니다. 그리고 개인적으로는 Boost 라이브러리에 포함되어 있는 것들이 왠지 좀 더 믿음직스러워서... :-) 이번 글에서는 Boost.Python 문서에 나와 있는 예제 를 가지고 간단하게 python 모듈을 만드는 방법에 대해서 알아보겠습니다. Requirements 리눅스 이 글에서는 리눅스 환경에서의 사용 방법을 설명한다. Boost.Python 라이브러리 (1.33.1) Boost 라이브러리를 다운로드받아 아래와 유사한 명령으로 라이브러리를 빌드한다. bjam -sTOOLS=gcc -with-python install bjam의 --prefix 옵션으로 라이브러리가 설치될 위치를 변경할 수 있다. Python 라이브러리 (2.4.3) Python을 다운로드 받아 빌드하여 설치한다. 위의 경우와 유사하게 configure의 --prefix 옵션으로 설치될 위치를 변경할 수 있다. Write C++ Code 다음과 같이 코드를 작성한다. // greet.cpp #include <stdexcept> char const* greet(unsigned x) { static char const* const msgs[] = { "hello", "Boost.Python", "world!" }; if (x > 2) throw std::range_error("