고수들이 절대 가르쳐 주지 않는 C/C++ 프로그래밍 팁 #13 - 로깅 라이브러리 빌드하기
고절가주팁 열세번째 시간입니다. 지난글을 통해 로깅 라이브러리를 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란 무엇인가를 클릭하세요.