고수들이 절대 가르쳐 주지 않는 C/C++ 프로그래밍 팁 #13 - 로깅 라이브러리 빌드하기

Posted at 2007. 9. 5. 01:56 // in S/W개발/고절가주팁 // by 김윤수


(약간 Update된 버전입니다. 첨부파일도 함께 Update했습니다)

고절가주팁 열세번째 시간입니다. 지난글을 통해 로깅 라이브러리를 Open Source 로 공개한다는 말씀을 드렸습니다. 소스를 공개한 이후 몇 분으로부터 빌드 관련 에러를 보고 받은 게 있어서 그 문제에 대해 말씀드리고, 제가 만든 빌드 파일을 공개하려고 합니다(로깅 라이브러리 관련된 글을 처음 보시는 분들은 본문에 나와 있는 로깅 라이브러리를 클릭해 보시면 이전 글들을 모두 보실 수 있습니다).

저는 로깅 라이브러리 관련 글을 작성하면서 계속해서 Microsoft Visual C++ 2005 Express 를 사용해서 다른 환경에서 빌드되는지를 확인하지 않았더니 아니나 다를까 에러가 나더군요. 제가 사용했던 건 C++ 표준 라이브러리에만 있는 것들을 사용했었기 때문에 에러가 나면 안되는 거지만 세상만사가 우리 뜻대로만 안되는 것 아니겠습니까? 표준을 따르지 않는 개발 환경이 있기 마련이지요. 게다가 첫번째 C++ 표준이 제정된 것이 다른 언어에 비해서는 짧은 역사를 가지고 있어서 아직까지도 표준을 제대로 지원하지 못하는 컴파일러들이 당연히 있을 거라는 생각이 드네요.

우선 첫번째 빌드 문제!

Visual Studio 6 에서 빌드하면 다음과 같은 에러가 발생하더군요.

c:\documents and settings\김윤수\my documents\내 작업\yslog\logsource.cpp(49) : error C2784: 'class std::mem_fun_t<_R,_Ty> __cdecl std::mem_fun(_R (__thiscall _Ty::*)(void))' : could not deduce template argument for '<Unknown>' from 'bool (__thiscal
l YSLog::LogListener::*)(class YSLog::LoggingEvent *)'


역시나 눈이 훽훽 돌아가는 에러 메시지입니다. 뭔 에러 메시지를 이렇게나 해석하기 어렵게 출력해 대는지... 제가 컴파일러 만든다면 절대 저런식으로 메시지 출력하지 않을텐데 말입니다(헉! 컴파일러 만든 분 한테서 돌 날라오는 군요!!)

에러가 발생했을 때 가장 먼저 확인해야할 건 무엇일까요 ? 자~ 문제 들어갑니다. 1) 코드, 2) 매뉴얼, 3) 컴파일러 버전, 4) 에러 메시지. 시간은 10초 드리겠습니다. 일, 이, 삼, 사, 오, 육, 칠, 팔, 구, 십. 삐~~~~~~~~ 답은 바로 4번입니다. 가장 먼저 에러 메시지가 뭔지 확인해 봐야겠죠. 개발자들이 개발 중에 문제점을 만나면 보통은 에러 메시지를 보지도 않고, 바로 코드로 가더군요. 그러면서 코드를 한참 들여다 보고 나서 이상하다 잘못된 게 없는데...? 하면 반문하는 걸 여러번 봤습니다. 그걸 보면서 저는 "에러 메시지에 소스코드명이랑 라인번호 있네. 그 라인에 뭐뭐뭐뭐가 잘못됐데. 좀 확인해 봐." 그럼 개발자는 바로 그곳을 확인해 보고는 대부분의 경우 문제를 발견하고서는 "어~ 이거 잘못됐네. 어떻게 아셨어요 ?" "에러 메시지에 나와 있잖아~" 종종 이런 웃기지도 않은 대화를 하곤 했답니다. 여러분은 이렇게 하지 않으시겠죠 ?

자~ 그럼 LogSource.cpp 의 49번째 라인으로 가보겠습니다. Visual Studio 6 에서는 에러 메시지를 더블 클릭하면 바로 그 라인으로 갑니다. 다음과 같은 코드가 나올 겁니다.

46: bool LogSource::unconditionalLog(LoggingEvent* pLogEv) const
47: {
48:   for_each(_lListenerPtr.begin(), _lListenerPtr.end(),
49:     bind2nd(mem_fun(&LogListener::publish), pLogEv));
50:
51:   return true;
52: }

위에 파랗게 표시한 부분이 뭐가 잘못됐을까요 ? 에러 메시지를 다시 찬찬히 살펴 보실까요 ? (설명의 편의를 위해 약간 간략화 시켰습니다)

'class mem_fun_t<_R,_Ty> mem_fun(_R (_Ty::*)(void))' : could not deduce template argument for '<Unknown>' from 'bool (YSLog::LogListener::*)(class YSLog::LoggingEvent *)

잘 살펴 보시면 mem_fun 에는 인자가 없는데, LogListener::publish 는 LoggingEvent * 를 인자로 갖기 때문에 에러가 발생한 걸 알 수 있습니다. 어~ 거참 이상하군요. 제가 기억하기로 mem_fun 함수 객체 어댑터는 인자가 있는 경우에도 쓸 수 있는데 말입니다. 그리고, 저는 Visual C++ 2005 Express 에서 컴파일을 이미 해 보았기 때문에 인자가 있는 경우에도 쓸 수 있는 것으로 분명히 기억하고 있습니다. 자 그럼 다음으로 확인해 봐야할 건 뭘까요 ? 그렇습니다. 매뉴얼을 찾아봐야 하겠지요. mem_fun을 찾아 봤더니 다음과 같이 나오는 군요.

mem_fun

template<class R, class T>
    mem_fun_t<R, T> mem_fun(R (T::*pm)());

The template function returns mem_fun_t<R, T>(pm).

이번에는 mem_fun_t 를 한 번 찾아보죠.

mem_fun_t

template<class R, class T>
    struct mem_fun_t : public unary_function<T *, R> {
    explicit mem_fun_t(R (T::*pm)());
    R operator()(T *p);
    };

The template class stores a copy of pm, which must be a pointer to a member function of class T, in a private member object. It defines its member function operator() as returning (p->*pm)().

음... 확실히 mem_fun 은 인자가 없는 멤버 함수에 대한 어댑터인 것 같습니다. mem_fun 이 어떻게 구현되어 있는지 확인해 봐야 겠습니다. <functional> 을 직접 확인해 봤더니 다음과 같이 mem_fun 이 정의되어 있네요.

  // TEMPLATE CLASS mem_fun_t
template<class _R, class _Ty>
 class mem_fun_t : public unary_function<_Ty *, _R> {
public:
 explicit mem_fun_t(_R (_Ty::*_Pm)())
  : _Ptr(_Pm) {}
 _R operator()(_Ty *_P) const
  {return ((_P->*_Ptr)()); }
private:
 _R (_Ty::*_Ptr)();
 };
  // TEMPLATE FUNCTION mem_fun
template<class _R, class _Ty> inline
 mem_fun_t<_R, _Ty> mem_fun(_R (_Ty::*_Pm)())
 {return (mem_fun_t<_R, _Ty>(_Pm)); }

위 정의로 보건대 확실히 mem_fun 은 인자를 받지 않는군요. 그 아래에 mem_fun1 을 확인해 봤더니 mem_fun1 이 하나의 인자를 받는 어댑터이네요.

  // TEMPLATE CLASS mem_fun1_t
template<class _R, class _Ty, class _A>
 class mem_fun1_t : public binary_function<_Ty *, _A, _R> {
public:
 explicit mem_fun1_t(_R (_Ty::*_Pm)(_A))
  : _Ptr(_Pm) {}
 _R operator()(_Ty *_P, _A _Arg) const
  {return ((_P->*_Ptr)(_Arg)); }
private:
 _R (_Ty::*_Ptr)(_A);
 };
  // TEMPLATE FUNCTION mem_fun1
template<class _R, class _Ty, class _A> inline
 mem_fun1_t<_R, _Ty, _A> mem_fun1(_R (_Ty::*_Pm)(_A))
 {return (mem_fun1_t<_R, _Ty, _A>(_Pm)); }

그렇다면 mem_fun 을 mem_fun1 으로 바꿔야겠군요. 그런데 이상하군요. 왜 Visual C++ 2005 Express 에서는 그냥 컴파일이 됐을까요 ? C++ 표준안 draft 버전에서는 mem_fun1 이라는 멤버 함수 어댑터가 있었는데, 실제 표준안이 최종 통과될 때는 mem_fun1 이 mem_fun 으로 바뀌었다는 군요. C++ 첫번째 표준안이 98년 제정되었고, Visual Studio 6가 98년에 출시되었으므로 Visual Studio 6 에 있는 STL 이 최종 표준안의 반영하지 못한 것으로 해석할 수 있을 것 같습니다.

이렇게 해서 문제의 원인은 파악했는데 이걸 어떻게 해결할까요 ? 우선 가장 쉽게 생각할 수 있는 해결책으로는 다음과 같은 방법을 생각할 수 있을 것 같네요.

#if defined(_MSC_VER) && (_MSC_VER <= 1200)
#define mem_fun mem_fun1
#endif


위 방법은 아주 간단히 해결할 수는 있지만, 나중에 mem_fun() 이 진짜로 필요하게 될 때는 문제가 발생할 소지가 있는 방법이라서 우리의 해결책에서 제외! 해야할 것 같군요.

또 다른 간단할 해결책으로 Visual C++ 의 predefined macro 인 _MSC_VER 값을 사용해서
아까 컴파일 에러 났던 부분을 다음과 같이 수정하면 어떨까요 ?

  for_each(_lListenerPtr.begin(), _lListenerPtr.end(),
#if defined(_MSC_VER) && (_MSC_VER <= 1200)
    bind2nd(mem_fun1(&LogListener::publish), pLogEv)
#else
    bind2nd(mem_fun(&LogListener::publish), pLogEv)
#endif
    );

맘에 드시나요 ? 전 왠지 구리게 느껴지네요. 매번 새로운 컴파일러에서 비슷한 문제점이 생길때마다 저런식으로 일일이 컴파일러의 predefined macro 를 체크해 봐야 한다는 게 별로 맘에 들지는 않네요. 더 우아한 해결책은 없을까요 ? 다음과 같은 방법은 어떨까요 ?

// Portability.h
#if defined(_MSC_VER) && (_MSC_VER <= 1200)
#define USE_MEM_FUN1
#endif

// LogSource.cpp
#include "Portability.h"
...
  for_each(_lListenerPtr.begin(), _lListenerPtr.end(),
#ifdef USE_MEM_FUN1
    bind2nd(mem_fun1(&LogListener::publish), pLogEv)
#else
    bind2nd(mem_fun(&LogListener::publish), pLogEv)
#endif
    );


위와 같이 Portability 와 관련된 내용을 한 곳에 몰아 두면 새로운 컴파일러나 OS 를 지원해야할 때, 소스코드 이곳 저곳을 편집할 필요 없이 Portability 관련 내용이 정의되어 있는 파일에 새로운 컴파일러나 OS 의 특성을 표현하게 하면 Porting 작업이 훨씬 수월해 질 것입니다. 여기에서 한 술 더 떠서 다음과 같은 방법도 가능할 것입니다.

#ifndef PORTABILITY_H
#define PORTABILITY_H

#if defined(_MSC_VER) && (_MSC_VER <= 1200)

#include <functional>

namespace std {

template<class _R, class _Ty, class _A> inline
 mem_fun1_t<_R, _Ty, _A> mem_fun(_R (_Ty::*_Pm)(_A))
 {return (mem_fun1_t<_R, _Ty, _A>(_Pm)); };

}

#endif

#endif // PORTABILITY_H

// LogSource.cpp
#include "Portability.h"
...
  for_each(_lListenerPtr.begin(), _lListenerPtr.end(),

    bind2nd(mem_fun(&LogListener::publish), pLogEv));


즉, 그냥 인자를 하나 갖는 mem_fun() 을 직접 Portability.h에서 구현해 버리는 것이지요. 이렇게 하면 이식성에 관련된 대부분의 사항이 Portability.h 안에 정의되고, 다른 코드에서는 나타나지 않게 되므로 코드가 훨씬 간결해지고, 고쳐야 하는 부분도 상대적으로 더 적어지게 될 것이므로 더 바람직한 방법이라고 판단됩니다.

다음은 Linux 를 위한 Build 파일(Makefile)!!

# Makefile
all: YSLog

OBJ_DIR = ./obj
SRC_DIR = ./src
INC_DIR = ./h

OBJECTS = $(OBJ_DIR)/Main.o \
 $(OBJ_DIR)/LogSource.o \
 $(OBJ_DIR)/SimpleLogSink.o

INCLUDES = -I$(INC_DIR)
OPTIONS = -Wall -g -c -O
CFLAGS = $(OPTIONS) $(INCLUDES)

YSLog: $(OBJECTS)
 g++ -o YSLog $(OBJECTS)

$(OBJ_DIR)/%.o:$(SRC_DIR)/%.cpp
 g++ $(CFLAGS) -o $@ $<

$(OBJ_DIR)/Main.o: $(INC_DIR)/LogSource.h $(INC_DIR)/LogPublisher.h \
  $(INC_DIR)/LogListener.h $(INC_DIR)/LoggingEvent.h \
  $(INC_DIR)/Portability.h

$(OBJ_DIR)/LogSource.o: $(INC_DIR)/LogSource.h $(INC_DIR)/LogPublisher.h \
  $(INC_DIR)/LogListener.h $(INC_DIR)/LoggingEvent.h \
  $(INC_DIR)/Portability.h $(INC_DIR)/SimpleLogSink.h

$(OBJ_DIR)/SimpleLogSink.o: $(INC_DIR)/SimpleLogSink.h \
  $(INC_DIR)/LogListener.h $(INC_DIR)/LoggingEvent.h

clean:
 rm -rf $(OBJ_DIR)/*.o YSLog



(Visual C++ 6 와 Linux에서 컴파일 가능한 소스 및 빌드 환경 첨부 합니다)


위 빌드 파일에 문제가 있을 경우 댓글로 알려 주시면 감사하겠습니다. 어떤 분은 g++ 3.2.2 에서 -O 옵션을 주고 컴파일하면 네임스페이스를 쓴 코드에서 에러가 난다고 하더군요. Linux에서 제가 쓴 컴파일러는 g++ 3.4.4 였는데, 문제 없이 컴파일되더군요.

제 글이 유익하셨다면 오른쪽 버튼을 눌러 제 블로그를 구독하세요. ->
블로그를 구독하는 방법을 잘 모르시는 분은 2. RSS 활용을 클릭하세요.
RSS에 대해 잘 모르시는 분은 1. RSS란 무엇인가를 클릭하세요.