일반

[C++] Template Class의 정의(Definition)와 구현(Implementation)은 한 파일 안에 있어야 한다.

Posted 2014. 10. 23 Updated 2015. 11. 26 Views 10786 Replies 0
?

단축키

Prev이전 문서

Next다음 문서

ESC닫기

크게 작게 위로 아래로 댓글로 가기 인쇄

※ 주의: 이 글은 지난 3일간 Link Error 때문에 코드 작성을 한 줄도 진행하지 못하고 삽질 again 삽질을 반복하는 딥빡침(?) 속에서 허우적대다가 겨우 헤어나서 작성하고 있는 글입니다. 다소 과격한 표현(?)이나 울분이 섞여있을수도 있습니다.

어디에서 오류가 발생했는지 파일명과 줄번호를 콕콕 찝어서 알려주는 Compile Error보다는 오류가 발생했는데 뭣때문인지 모르겠다는식의 메시지만 뱉어내는 Link Error는 마주치게 되면 일단 당황하고, 곧이어 깊은 빡침이 이어지게 됩니다.

이번에 제가 하고 있던 작업은 TI에서 제작한 TM4C1294XL LaunchPad에 연결한 TFT-LCD를 구동하기 위한 드라이버를 만들면서 발생했습니다. LCD들이 Character LCD, Graphic LCD, TFT-LCD 할 것 없이 구동 방법이 유사한 관계로, 코드 재사용과 모듈화에 조금 욕심을 냈던 것이 이번 3일간 딥빡침의 원인이었던 것 같습니다. (한편, 야매로 C++을 하고 있는 제 탓인지도..ㅜㅠ)


소스코드

문제가 된 소스코드의 구성은 다음과 같습니다.

Template Class인 FrameBuffer Class를 따로 구현하였고, 이를 LCDController Class에서 상속해서 Pure Virtual Method들을 구현한 뒤 내부적으로 사용합니다.

Frame Buffer Class를 Template으로 정의한 이유는 LCD에 따라서 Data Bus가 8bit 혹은 16bit로 다르므로, 추후 재활용을 염두해 두고 코드를 작성했기 때문입니다.

#ifndef _FRAME_BUFFER_HPP_
#define _FRAME_BUFFER_HPP_

#include <stdint.h>

namespace NS_FrameBuffer
{

typedef uint8_t FrameBufferCommand;
typedef uint8_t FrameBufferParameter;

template <typename DataBusWidth>
class FrameBuffer
{
protected:
	void WriteCommand(const FrameBufferCommand command);

...(중략)...

	virtual void WriteCommandTransaction(const FrameBufferCommand command) = 0;

...(중략)...

};

} // namespace NS_FrameBuffer

#endif // _FRAME_BUFFER_HPP_


#include <cstddef>

#include <stdint.h>
#include <stdbool.h>

#include "FrameBuffer.hpp"

namespace NS_FrameBuffer
{

template <typename DataBusWidth>
void FrameBuffer<DataBusWidth>::WriteCommand(const FrameBufferCommand command)
{// Wrtie 8-bit command (RS=0)
	WriteCommandTransaction(command);
}

...(중략)...

} // namespace NS_FrameBuffer


#ifndef _LCD_CONTROLLER_HPP_
#define _LCD_CONTROLLER_HPP_

#include <stdint.h>

#include "FrameBuffer.hpp"

namespace NS_LCDController
{
using namespace NS_FrameBuffer;

class LCDController : public FrameBuffer<uint16_t>
{
public:
	void Init();

...(중략)...

private:
	virtual void WriteCommandTransaction(const FrameBufferCommand command);

...(중략)...

};

} // namespace NS_LCDController

#endif // _LCD_CONTROLLER_HPP_


#include "FrameBuffer.hpp"
#include "LCDController.hpp"

namespace NS_LCDController
{
using namespace NS_FrameBuffer;

void LCDController::Init()
{
	...

	// ** Unresolved Symbol Error 발생 **
	this->WriteCommand(0x01);

	...
}

...(중략)...

void LCDController::WriteCommandTransaction(const FrameBufferCommand command)
{
	*FB_CMD_PTR = (uint16_t)command;
}

...(중략)...

} // namespace NS_LCDController


문제 발생과 원인 분석

이렇게 코드를 작성하고 Build 버튼을 눌렀을 때 제 앞에 나타난 것은 다름 아닌 무시무시한 Link Error였습니다.

오류 내용인 즉, Symbol 'NS_FrameBuffer::FrameBuffer<unsigned short>::WriteCommand(unsigned char)'가 정의되지 않았으며, 'LCDController.obj'에서 최초로 참조되었다는 내용이었습니다.

환장할 만도 했던 것이, 뻔히 FrameBuffer.cpp에 이 Method를 정의해 놓았고, Compile Log에서 이 파일을 아무 문제 없이 컴파일 하는 것을 수십번도 더 확인했기 때문입니다.

처음에는 Template Class를 상속한 Class에서 부모 Class의 Method를 호출하는 방법이 잘못되었나 싶어 이와 관련한 솔루션을 찾아 구글링을 했습니다. 하지만 이 부분은 아무 문제가 없었습니다.

this->WriteCommand(0x01); 이걸 WriteCommand(0x01);로도 바꿔 보고, FrameBuffer<FrameBufferData>::WriteCommand(0x01);로도 바꿔 보고, Namespace로 다 없애보고 등등...

갖은 시도를 다 하면서 정작 코딩 진행은 하나도 못하면서 3일째가 되자, 총체적 멘탈붕괴 상태가 되었습니다. 으어어어아아아... 작업하려고 컴퓨터 앞에 앉았는데, 더 이상 찾아보고 삽질할 멘탈이 없어서 괜히 페북 들락거리고 헛짓하고. 그렇게 시간을 보내게 되었습니다.

나중에는 '컴파일러가 C++ 표준을 따르고 있지 않은가?' 라는 의문까지 갖게 되었습니다.ㅡㅡ;;
(하지만 VS에서 테스트 해보고 컴파일러 문제가 아니라는걸 확인)


나중에는 컴파일된 Object File에 WriteCommand라는 Symbol이 있기는 한지 의문을 품고 Object File들을 직접 뜯어보기에까지 이르렀습니다. [...]

당연히 있을거라고 생각했는데 웬걸, 그런 심볼은 Object파일 안에 없었습니다. Object File에 Symbol이 존재하지 않는다는 것은, 해당 Method의 구현이 아예 컴파일되지 않았다는 것입니다.

여기에서 실마리를 얻고, 다시 몇시간 구글링과 삽질을 한 끝에 원인을 알아내고 오류를 고칠 수 있었습니다.


결론을 먼저 말하자면, 문제는 Template Class의 상속이 아닌 Template Class 자체에 있었습니다. 핵심은 'Template Class의 정의와 구현은 한 파일 안에 있어야 한다.' 라는 점입니다.

보통 C++에서는 여러 가지 장점 때문에 Class의 정의가 있는 헤더 파일과 구현이 있는 소스 파일을 분리해서 작성합니다. 이렇게 분리하더라도 컴파일 이후 링커가 컴파일된 소스파일들을 하나로 연결해 주기 때문에 문제가 없는 것이지요.

여기서 짚고 가야 할 중요한 사실이 하나 있는데, 바로 'Template Class는 Class가 아니다.' 라는 사실입니다. 이게 뭔소린가 하면, Template Class는 Class를 찍어내기 위한 '틀'일 뿐, Class 자체는 아니라는 것입니다.

컴파일러는 파일 단위로 컴파일을 진행하기 때문에 어떤 헤더파일이 어떤 소스파일과 짝인지 여부는 고려하지 않고 컴파일을 합니다. 그것도 헤더파일은 따로 컴파일하지 않으며, 컴파일 대상은 오직 소스파일들(*.c, *.cpp)뿐입니다.

소스파일에 헤더파일이 Include 되어 있다면, 헤더파일을 그대로 복붙하고 컴파일을 진행할 뿐입니다. 어떠한 소스파일에도 포함되지 않은 헤더파일이 존재한다면, 그 헤더파일은 컴파일되지 않습니다.


위의 소스코드를 보면, LCDController.cpp에서 FrameBuffer.hpp를 Include하였습니다. FrameBuffer.hpp에는 FrameBuffer Template Class의 원형이 정의되어 있고, LCDController에서 상속할 때 Parameter를 명시해 줬으므로 컴파일 과정에서 DataBusWidth = uint16_t인 FrameBuffer Class가 생성됩니다.

단, 이 때 FrameBuffer Class의 Method들은 정의되지 않았으므로 이후 과정은 링커에게 넘기게 되며, LCDController의 컴파일 과정은 오류 없이 종료됩니다.

다음 과정으로 FrameBuffer.cpp의 컴파일을 진행하는데, 여기에도 역시 FrameBuffer.hpp가 포함되어 있습니다. 이 컴파일 과정에서 FrameBuffer Template Class는 정의되지만, 어디에도 Template Class Parameter인 DataBusWidth를 명시해서 FrameBuffer Class를 찍어내는 구문을 찾을 수 없습니다.

이 컴파일 과정에서 FrameBuffer를 찍어내는 틀(Template)은 한 번도 사용되지 않고 그냥 버려지게 됩니다. 즉, 컴파일 결과물에 FrameBuffer Class는 존재하지 않으며, Class Method들도 하나도 정의되지 않습니다. (DataBusWidth를 알 길이 없으니, 정의할 수 없는것입니다.)

결과적으로 LCDController.cpp를 컴파일할 때 다른 파일에 FrameBuffer Class Method들이 정의가 되어 있을 것으로 예상하고 컴파일을 마쳤으나, 정작 FrameBuffer.cpp를 컴파일할때는 FrameBuffer Class가 생성조차 되지 않았습니다. 따라서 링크 과정에서 당연히 Symbol을 찾을 수 없다는 오류가 발생하게 되는것이지요.


해결 방법

Template Class의 이런 미묘한 문제를 해결하기 위해서는 컴파일 과정에서 컴파일러가 Template Class로부터 Class를 생생하고 컴파일하도록 유도해야 합니다.


Dummy Class를 정의해서 해결하기

가장 간단한 해결책으로, FrameBuffer.cpp에 DataBusWidth = uint16_t인 FrameBuffer Class를 정의하는 구문을 삽입하는 방법을 생각할 수 있습니다.

...
FrameBuffer<uint16_t> FrameBuffer16;
...

이렇게 하면 컴파일러가 16bit짜리 FrameBuffer Class를 생성하므로 링크 과정에서 오류가 발생하지 않을 것입니다.

하지만 FrameBuffer Class는 Pure Virtual Method들을 포함하고 있기 때문에 이런 방법은 사용할 수 없습니다. 링크 과정에서 오류가 안나더라도 컴파일 과정에서 오류가 날 것이므로, 링크를 시도조차 할수 없겠죠.

이 방법은 Pure Virtual Method를 포함하고 있지 않은 Class에 대해서 사용할 수 있습니다. 단, 이렇게 할 경우 프로그램 전체에서 사용할 Class를 Paramter별로 모두 이렇게 명시해 주어야 합니다. 게다가 사용하지 않는 Dummy Code를 집어넣어야 하기 때문에 뭔가 깔끔하지 못한 기분이 듭니다.


구현을 헤더파일에 포함시키기

다음으로, 제가 이번 문제를 해결한 방법입니다. 제목에서 언급한것과 같이, 헤더파일의 아랫부분에 소스코드를 포함시켜 버리는 것입니다.

헤더파일에 소스파일에 포함시키는 경우, 컴파일 오류가 발생하지 않도록 빌드 과정에서 소스파일을 제외시켜 줘야 합니다. 그렇지 않을 경우, Symbol Redifinition오류가 발생하면서 컴파일 자체가 되지 않습니다.

구글링을 통해 알아본 결과, 보통 이러한 경우 파일 확장자인 cpp를 tpp로 바꿔서 빌드에서 제외함과 동시에 Template의 구현이라는 사실을 나타내는 방법을 많이 사용하고 있었습니다.

저는 FrameBuffer.cpp 파일을 FrameBuffer.tpp로 바꾸고, FrameBuffer.hpp 하단에 다음과 같이 Include 시켰습니다.

#ifndef _FRAME_BUFFER_HPP_
#define _FRAME_BUFFER_HPP_

#include <stdint.h>

namespace NS_FrameBuffer
{

typedef uint8_t FrameBufferCommand;
typedef uint8_t FrameBufferParameter;

template <typename DataBusWidth>
class FrameBuffer
{
protected:
	void WriteCommand(const FrameBufferCommand command);

...(중략)...

	virtual void WriteCommandTransaction(const FrameBufferCommand command) = 0;

...(중략)...

};

} // namespace NS_FrameBuffer

#include "FrameBuffer.tpp"

#endif // _FRAME_BUFFER_HPP_

그리고 빌드를 시도해 본 결과, 3일동안 봐 왔던 지긋지긋한 Undefined Symbol 오류가 사라지고 링크까지 깔끔하게 끝나는 것을 볼 수 있었습니다.


결언

문제에 대한 원인을 알게 된 직후로 멍청한 컴파일러! 라고 생각하기도 했지만, 왜 그런지 찬찬히 생각해본 끝에 왜 그런지 깨닫고 고개를 끄덕일 수 있었습니다.

헤더파일과 소스파일을 나누는 이유는 소스코드 관리를 수월하게 하고, Incremental Build와 같이 컴파일을 효율적으로 하기 위함입니다. 오늘날 C++에서 제공하는 Template Class의 구현 방법을 사용하려면 어느정도의 비효율을 감수해야 할 것입니다.

만약 Template Class의 Method 구현부가 아주 길다면, 게다가 정의한 Template Class를 Include해서 사용하는 다른 파일들이 아주 많다면, 빌드를 할 때마다 이 Template Class 전체를 다시 컴파일하게 되기 때문에 상당한 작업 시간이 소요될 것입니다.

Template Class와 관련해서는 C++ 표준을 고쳐서 정의와 구현이 분리가 가능하도록 해야하지 않을까 라는 생각을 해 봅니다.


TAG •