블로그 이미지
대갈장군

calendar

1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31

Notice

'2014/08'에 해당되는 글 2

  1. 2014.08.28 typename에 대해 좋은글 발견!
  2. 2014.08.07 Headers and Includes: Why and How
2014. 8. 28. 04:47 프로그래밍/C++

출처: http://ikpil.com/540


내가 C++에 조예가 깊어서 글을 남기는 것이 아니라, Effecitve C++ 을 공부하는 사람들이 이 글을 보고, 도움이 되었으면 하는 생각과, 혹시 내가 틀린것이 있다면 지적해 주시지 않을까 란 생각으로 글을 올리는것임을 미리 밝힙니다.  - 최익필


typename .. 뭐 이렇게만 보면 이런게 있었나 싶다. 하지만 템플릿에서는 흔하게 보는 키워드인데, 이 키워드에 대해서 제대로 파악하자고 하는것 같다.

처음부터 진행하자면 typename 과 class 는 똑같은 의미인데, 이렇게 말만 하면 혼동의 요지가 있으니, 코드도 포함해 주는 센스를 발휘해 본다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// ikpil.com or ikpil.tistory.com
 
#include <iostream>
//template <typename _T>  // class _T 와 같은 의미
template <class _T>
class babo
{
typedef _T value_type;
public:
    void Draw( void )
    {
        std::cout << sizeof(value_type) << "의 용량을 가진 클래스" <<std::endl;
    }
private:
    _T *p;
};
 
int main( void )
{
    // int 타입을 가진 babo 클래스의 객체 Test 선언
    babo<int> Test;
     
    // 뭐~ 출력
    Test.Draw();
 
    return 0;
}


위의 주석을 다 읽어 보면 무슨 말인지 알듯 싶다. 하지만 스콧마이어어가 쓰고, 곽용재씨가 번역한 이번 항목에선 typename 은 또 다른 의미를 지니고 있다고 알려 주려고 하는듯 하다.

그렇다 typename 키워드는 .. ... 은 타입이다! 를 알려 주는 용도로 사용 한다. 요렇게만 들으면 또 .. 햇갈린다. 더 이야기 하자면 typename 키워는 템플릿 선언 내부에서만 사용 할수 있다는 것을 기억해야 한다. 아래 소스코드를 첨부 하겠다.

1
2
3
4
5
6
7
8
9
10
11
12
13
// ikpil.com or ikpil.tistory.com
int main( void )
{
    // int 타입을 가진 babo 클래스의 객체 Test 선언
    babo<int> Test;
     
    typename int a; // 컴파일 에러를 볼수 있을 것이다.
 
    // 뭐~ 출력
    Test.Draw();
 
    return 0;
}


그렇다면 이것은 템플릿 내부에서 써보자면

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ikpil.com or ikpil.tistory.com
 
#include <iostream>
template <typename _T>    // class _T 와 같은 의미
class babo
{
typedef _T value_type;
public:
    void Draw( void )
    {
        std::cout << sizeof(value_type) << "의 용량을 가진 클래스" <<std::endl;
    }
private:
    _T *p;
public:
    typename int hehehe;    // 이렇게 쓸수 있다.
};


이렇게 쓸수 있다는 것인데, 이것만 본다면, 
1. int 는 타입이다
2. 그 타임으로 a 객체 생성!

.. 이것을 보고 비웃지 않는다면, 당신이야 말로 성인(成人)이다. 왜냐하면 int 는 누가봐도 타입인데 굳이 typename 을 피곤하게 붙인다는 것에 대해서 말이다.

그렇다면 다음 코드는 어떻게 이해 할 것인가.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// ikpil.com or ikpil.tistory.com
#include <iostream>
#include <vector>
 
template <typename _T>    // class _T 와 같은 의미
class babo
{
public:
    void Draw( void )
    {
        std::cout << sizeof(p) << "의 용량을 가진 클래스" <<std::endl;
    }
private:
    _T *p;
public:
    _T::iterator muhaha; // 이것 때문에 컴파일이 되지 않는다.
};
 
int main( void )
{
    // int 타입을 가진 babo 클래스의 객체 Test 선언
    babo<std::vector<int> > Test;
     
    // 뭐~ 출력
    Test.Draw();   
 
    return 0;
}


.. 물론 나는 _T의 타입인 iterator 란것을 분명이 알고 있다. 하지만 컴파일러는 다르다. 어떤 초특급 울트라 천재가 컴파일러를 만든다 하더라도, .. 저것이 "타입" 인것을 .. 알려면 몇달 몇일 간 별 수를 다 생각해야 할것이다. 

_T::iterator muhaha; 이것을
typename _T::iterator muhaha; 라고 바꾸면 정상적으로 컴파일이 되는 것을 볼수 있을것이다. 그렇다! typename은 템플릿 내부에서 사용하면서, 컴파일러에게 템플릿 내부에서 "이건 타입이야" 라고 알릴 필요가 있을 때 써준다.


이쯤에서 용어 설명을 하는게 좋을듯 싶은데,
_T 처럼 타입을 인자로 받아 그것을 타입으로 쓰기 때문에 _T를 의존 이름(dependent name) 이라 외국에서 불린다.요~런 사항에서 의존 이름(dependent name)속에 또 다시 다른 이름이 정의된 경우 그녀석은 중첩 의존 타입 이름(nested dependent type name .. 뭐 외국 따라가야지, .. 태생이 외국인인데..)이라고 불리는데,

바로 템플릿 내부에서 중첩 의존 타입 이름(nested dependent type name)이 있을 경우, 그녀석도 타입인것을 인지하지 못하는 사태가 발생 할 수 있다.(발생 할수 있다는 이유는 MSVC2005에선 발생하지만, 다른 컴파일러에선 발생 안할수도 있다는 뜻이다. 뭐.. 대부분은 발생한다.)

여담으로 int 같은 것들은 비의존 이름(non-dependent name)이라고 불린다. .. 뭐 그런게 있다~ 라고 알아두면 좋을듯 싶고.. 여기서 한가지 짚고 넘어가야 한다. 이런 typename 을 쓸때, template 내부여도 못쓰는 경우가 두가지 있다는 것이다!

이 경우는 중첩 의존 타입 이름(nested dependent type name)이 기본 클래스의 리스트에 있거나, 멤버 초기화 리스트 내의 기본 클래스 식별자로 있을경우 typename을 붙여 주면 안된다는 것이다!(사실 이 구역은 컴파일러가 알아서 이녀석은 타입이다! 라고 알수 있는 곳이기도 하다.) 이렇게만 말하면 다시 .. 난해 할 수 있으니 코드를 달아 주는 센스!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// ikpil.com or ikpil.tistory.com
// typename 을 예외적으로 사용 할수 없는
// 구역을 나타내기 위해서 이렇게 코드를 만든다.
 
#include <iostream>
#include <vector>
 
template <typename _T>
class NON_TYPENAME
{
public:
    // 뭐 클래스 네임만 신경쓰고 나머진
    // 무시해도 된다.
    NON_TYPENAME() { }
    // 소멸자
    ~NON_TYPENAME() { }
private:
    _T m_p;
};
 
 
template <typename _T>    // class _T 와 같은 의미
class babo
    : public NON_TYPENAME<_T> // <-- 여기가 기본 클래스의 리스트
{
public:
    babo()
        : NON_TYPENAME<_T>() // <-- 여기가 멤버 초기화 리스트
    {
 
    }
    void Draw( void )
    {
        std::cout << sizeof(p) << "의 용량을 가진 클래스" <<std::endl;
    }
private:
    _T *p;
public:
    typename _T::iterator muhaha;   // <-- 여긴 사용 할수 있다~
};
 
int main( void )
{
    // int 타입을 가진 babo 클래스의 객체 Test 선언
    babo<std::vector<int> > Test;
     
    // 뭐~ 출력
    Test.Draw();   
 
    return 0;
}


자.. 이제 이해가 되었는가? 여기에 코딩 스타일이 좀더 편해질만한 이야기를 하나 더 들려 준다.
바로 typename _T::iterator muhaha; 를 쓸때 한번이나 두번 쯤이야 써주겠지만 3 이상 넘어가면 .. 슬슬 피곤해 진다. 이럴 때는 typedef typename _T::iterator iterator; 라고 쓰고 iterator muhaha; 라고 쓰면. 뭐 쉽지 않니한가? ㅋㅋ


여담으로 몇가지 더 이야기를 한다.
이 이야기는 곽용재씨가 들려주는 이야기로 스콧 마이어스와 허브서터, 안드레 알렉산드레스쿠는 사용자가 정의한 타입에 대해서는 class 를 쓰고 기본 제공 타입에 대해서는 typename 을 쓴다. 그리고 비야네 스트롭스트룹은 class 를 주로 쓰는 편이라고 한다;

이렇게 굳이 꼭 정해 둘 필요가 있을까? 하는 사람도 있지만, 코드가 몇만줄 넘어가면, 조금이라도 코드에 정보 아닌 정보를 남겨두는 편이, 디버깅이나 유지보수 등에 도움이 될때가 많다. 이런 습관도 있으니 배워두자는 취지에서 곽용재씨가 남겨 주셧던것 같다.


이것만은 잊지 말자!
1. 템플릿 매개변수를 선언할 때, class 나 typename 이나 똑같다.
2. 중첩 의존 타입 이름(nested dependent type name)을 식별하는 용도는 typename 키워드이다.
3. typename 은 템플릿 내부에서만 사용 되고, 초기화리스트 및 기본 클래스 리스트에서는 못쓴다!


관련링크
http://www.ibm.com ... typename keword <-- ibm의 typename 설명서 아, 영문이다 제길
http://pages.cs.wisc.edu/~driscoll/typename.html <-- 여기도 외국 . typename 설명서 .. 영문이다.
http://keiphoto.egloos.com/4182181 <-- 짤막한 .. 영문으로.
http://kukuta.tistory.com/12 <-- 오오! 괜찮은 설명~ 아 여긴 한국
http://codefactory.zc.bz/50 <-- 댓글이 설명이 좋았음
http://rein.upnl.org/wordpress/archives/494 <-- typename 관련 글은 아니지만, 템플린 관련해서 좋은 글
http://www.filewiki.net/tc/34 <-- typename 을 어떤 식으로 사용하는지 소스로 확인 할수 있음
http://dblab.co.kr/119 <-- 역시 마찬가지
http://kldp.org/node/62341 <-- typename 문법 없이 사용할 경우, 질문 내용
http://kldp.org/node/39641 <-- 역시 질답 내요이 좋음

posted by 대갈장군
2014. 8. 7. 22:57 프로그래밍

출처: http://www.cplusplus.com/articles/Gw6AC542/


 1> 왜 우리는 헤더 파일이 필요한가? 


만약 당신이 C++ 처음 접하는 사람이라면 프로그램이 왜 #include 를 하는지와 여러개의 .cpp 파일이 필요한 지 궁금할 것이다. 이유는 다음과 같다.


  1. Compile 속도를 높인다. 당신의 프로그램이 커지면 커질수록 코드의 양도 많아진다. 만약 모든 것이 하나의 파일에 다 들어간다면 작은 변경에도 큰 파일 전체가 컴파일 되어야 하므로 시간이 오래 걸린다. 물론 작은 프로그램의 경우에는 이거 별거 아니지만 적당한 규모 이상일 경우에는 컴파일 타임을 무시 할 수 없다.
  2. 코드가 보다 잘 정렬된다. (Organized) 만약 내가 코드의 각 부분을 컨셉에 따라 잘 나누어 놓으면 어떤 파일을 변경해야 하는지 손 쉽게 알 수 있으므로 잘 정리된 프로그램은 수정이 용이 하다.
  3. interface를 implementation으로 부터 분리 할 수 있다. 즉, 헤더에 프로그램의 interface를 정의해 주고 cpp 코드에 implementation을 하면 아주 깔끔하다.
C++ 프로그램들은 두 단계를 거쳐 생성된다. 첫번째 단계는 각각의 소스 파일들이 각각 컴파일 되는 것이다. 컴파일러는 각각의 컴파일된 소스파일에 대한 intermediate file들을 생성해 낸다. 이 중간 단계의 파일들을 우리는 object file이라고 부른다. 그리고 일단 모든 파일들이 각각 컴파일 된 후에는 모든 object 파일들을 링크하게 된다. 바로 이때 Binary 파일 (exe 파일)이 생성된다.


자, 좀 더 자세히 설명하자면, 각각의 소스 파일이 다른 소스 파일과는 완전히 별개로 컴파일 된다. 이말인 즉슨, a.cpp라는 소스 파일은 b.cpp라는 소스 파일 안에 무슨 일이 벌어지고 있는지 전~혀 모르고 있다는 말이다. 예를 들자면,


// in myclass.cpp

class MyClass
{
public:
  void foo();
  int bar;
};

void MyClass::foo()
{
  // do stuff
}


// in main.cpp

int main()
{
  MyClass a; // Compiler error: 'MyClass' is unidentified
  return 0;
}



위 두 파일의 경우 분명히 MyClass가 myclass.cpp에 선언되었지만 main.cpp에서는 이 클래스에 대해서 알수 있는 길이 없다는 것. 즉, 에러가 난다. 너무나도 당연한 에러다.


여기서 헤더파일의 역활이 드러난다. 헤더 파일은 다른 .cpp 파일들에게 interface를 공개하는 역활을 한다. 물론 implementation은 cpp파일에 남겨두고 오직 interface만 공개하는 것이 헤더 파일의 임무이다. 위의 경우를 예를 들자면 헤더 파일은 다음과 같아야 한다.


// in myclass.h

class MyClass
{
public:
  void foo();
  int bar;
};


// in myclass.cpp
#include "myclass.h"

void MyClass::foo()
{
}


//in main.cpp
#include "myclass.h"  // defines MyClass

int main()
{
  MyClass a; // no longer produces an error, because MyClass is defined
  return 0;
}


자, 이제 main.cpp에서 MyClass 클래스에 대해서 구조를 알 수 있으므로 컴파일에 에러가 나지 않는다.



 2> 확장자에 따른 차이점은?  


그렇다면 여기서 궁금한 것 한가지, .h / .cpp / .hpp / .cc 등등과 같은 서로 다른 확장자의 파일들은 무슨 차이가 있는가?


생각보다 룰이 간단한데, 모든 .h__ 파일들은 걍 헤더 파일이다.

모든 .c__ 파일들은 모두 C++ 소스 코드들이다. 무슨 확장자던 관계없다.

C 코드는 반드시 .c 파일이어야 한다. (요거만 예외적이네)


  3> Include Guards


C++ 컴파일러는 뇌가 없다. 우리가 시키는 대로만 하는 것이 컴파일러이다. 고로 헤더를 두번 include 시키게 되면 이미 정의된 놈이라는 어이없는 에러가 마구마구 터질 것이다. 아마도 어떤 바보가 이렇게 하겠느냐고 질문하겠지만 이런 경우는 의외로 빈번하게 발생한다. 예를 들자면 다음과 같은 경우이다.


1
2
// x.h
class X { };


1
2
3
4
// a.h
#include "x.h"

class A { X x; };


1
2
3
4
// b.h
#include "x.h"

class B { X x; };


1
2
3
4
// main.cpp

#include "a.h"  // also includes "x.h"
#include "b.h"  // includes x.h again!  ERROR 


a.h와 b.h가 둘다 x.h를 include 하는 상황에서 main.cpp에서 a.h와 b.h를 두 번 include 하게 되면 x.h가 두 번 불러지는 셈이된다. 고로 에러...


종종 어떤 사람들은 헤더 파일에서는 include를 하지 말라는 터무니 없는 말을 하는데 이것은 잘못된 지식이다. 헤더에 무슨 파일을 포함하던지 상관이 없다. 단, 두 가지 조건을 만족해야 한다.


1. 반드시 필요한 헤더만 include하라

2. include guards를 통해서 multiple include를 미연에 방지하라


여기서 처음 보는 단어의 등장. Include Guards라는 것인데, 이것은 다음과 같은 것이다.


1
2
3
4
5
6
7
8
//x.h

#ifndef __X_H_INCLUDED__   // if x.h hasn't been included yet...
#define __X_H_INCLUDED__   //   #define this so the compiler knows it has been included

class X { };

#endif 


자, 이렇게 헤더를 둘러쌓으면 x.h가 두번째 include 될때 __X_H_INCLUDED__가 이미 정의되었으므로 헤더가 통째로 스킵되어 버린다. 고로 에러가 없이 지나간다. 고로, 무조건 include guard를 해라, 모든 헤더파일에 해라. 해도 무해하다... 해라 해라 또 해라.



  4> 올바른 include 방법


내가 만드는 클래스는 종종 다른 클래스들에게 의존적인 경우가 많이 있을 것이다. 예를 들자면 파생 클래스의 경우 부모 클래스를 알고 있어야 한다. 부모가 있어야 자식이 나오기 때문이지.


일단 두 가지 종류의 Dependency 가 있는데 다음과 같다.

1. Forward declare가 가능한 것

2. #include가 되어야만 하는 것


자, 예를 들어 Class A가 Class B를 사용하고 있다면 Class B는 Class A의 Dependency 중 한 개다. 그렇다면 Class A 내부에서 B를 어떻게 사용하는 가에 따라 forward declared 될 것인지 아니면 included 되어야 할지 분류가 가능하다. 다음과 같은 룰을 따르면 된다.


1. 만약 A가 B에게로 어떤 reference 도 만들지 않았을 경우: 아무것도 할 필요 없음

2. 만약 B에게 향하는 유일한 reference 가 friend declaration인 경우: 아무것도 할 필요 없음

3. 만약 A가 B 포인터 혹은 레퍼런스를 포함한 경우: Forward Declare

4. 만약 하나 혹은 하나 이상의 함수가 B의 object/pointer/reference를 입력 변수로 가진 경우 (또는 리턴 타입으로 가지는 경우): Forward Declare

5. 만약 B가 A의 부모 클래스라면: #include "b.h"

6. 만약 A가 B의 객체를 포함한다면: #include "b.h"


자, 말로 설명하니 좀 이해가 안된다. 코드로 보면, 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//=================================
// include guard
#ifndef __MYCLASS_H_INCLUDED__
#define __MYCLASS_H_INCLUDED__

//=================================
// forward declared dependencies
class Foo;
class Bar;

//=================================
// included dependencies
#include <vector>
#include "parent.h"

//=================================
// the actual class
class MyClass : public Parent  // Parent object, so #include "parent.h"
{
public:
  std::vector<int> avector;    // vector object, so #include <vector>
  Foo* foo;                    // Foo pointer, so forward declare Foo
  void Func(Bar& bar);         // Bar reference, so forward declare Bar

  friend class MyFriend;       // friend declaration is not a dependency
                               //   don't do anything about MyFriend
};

#endif // __MYCLASS_H_INCLUDED__ 


위에서 설명한 모든 경우에 대해서 잘 보여주고 있다. Forward Declare는 헤더 시작 부분에 먼저 정의 하라는 의미네. 여기서 명심해야 할 점은, Foo class의 경우 MyClass 본체에서 포인터로만 사용되고 있기 때문에 include를 할 필요가 없고 다만 forward declare만 해주면 된다는 점. 최대한 include를 최소화 하는 방향으로 가는 것이 가장 올바른 코딩이다. 무분별한 include는 문제를 야기 할 수 있다는 점 명심하란다.



  5> 왜 위의 방법이 '올바른' 방법인가?


그렇다면 왜 필자가 말하는 방법이 이른바 '올바른' include 방법인가? 일단 해답은 Object Oriented에 있다. 필자가 이루고자 하는 기본적인 것은 myclass.h 파일이 그 자체로 self-contained, 즉, 완전체가 되기 위해서는 위와 같은 법칙을 따라야 한다는 것이다. 만약 다른 소스 파일에서 myclass를 써야 한다면 #include "myclass.h" 단 한줄로 모든것을 끝내버릴 수 있다. 


반면 헤더 파일안에 헤더 파일을 include 하지 않는 원칙을 따르게 되면 다음과 같은 문제가 생긴다. myclass를 사용하려면 myclass가 필요로 하는 모든 헤더 파일을 먼저 추가해야 하는 문제가 발생한다. 다음과 같은 코드를 보자.

1
2
3
4
5
6
7
//example.cpp

//  I want to use MyClass
#include "myclass.h"   // will always work, no matter what MyClass looks like.
                       // You're done
               //  (provided myclass.h follows my outline above and does
               //   not make unnecessary #includes) 


위의 코드를 보면 myclass.h를 포함시켰는데 만약 필자가 제시한 방법대로 했다면 다른 어떤 파일도 추가로 필요하지 않지만 만약 그렇지 않은 경우라면 다음과 같은 이유로 에러가 빠빡 터진다.


Here is an example of why so-and-so's method is bad:

1
2
3
4
5
//example.cpp

//  I want to use MyClass
#include "myclass.h"
   // ERROR 'Parent' undefined 



so-and-so: "Hrm... okay...."

1
2
3
#include "parent.h"
#include "myclass.h"
   // ERROR 'std::vector' undefined 



1
2
3
4
#include "parent.h"
#include <vector>
#include "myclass.h"
   // ERROR 'Support' undefined 



so-and-so: "WTF? MyClass doesn't even use Support! But alright..."

1
2
3
4
5
#include "parent.h"
#include <vector>
#include "support.h"
#include "myclass.h"
   // ERROR 'Support' undefined 


제일 마지막 줄에 Support가 없어서 에러가 터지는 이유는 parent.h에서 support 클래스를 사용하고 있기 때문이란다. 고로 support.h가 parent.h 보다 앞에 include되어야 에러가 안난다. 자, 고로 문제는 헤더 파일에 다른 헤더 파일을 포함하지 않는 경우, 하나의 클래스를 사용하기 위해 해당 클래스가 사용하는 모든 헤더파일을 먼저 불러와야 한다는 단점과 더불어 헤더 파일의 순서 또한 중요하다는 것을 알 수 있다. 이런 문제는 사실 비일비재 하게 발생하고 있다.



  6> Circular Dependencies


1
2
3
4
// a.h -- assume it's guarded
#include "b.h"

class A { B* b; };


1
2
3
4
// b.h -- assume it's guarded
#include "a.h"

class B { A* a };


상호 의존이라 함은 위의 예제 코드 처럼 서로가 서로의 클래스르 사용하는 경우를 말한다. 그냥 보기에는 문제가 없어 보이지만 실제로 컴파일러의 흐름을 따라가보면 아래와 같은 문제가 발생한다.


1
2
// a.cpp
#include "a.h" 


The compiler will do the following:

1
2
3
4
5
6
7
8
9
10
11
12
#include "a.h"

   // start compiling a.h
   #include "b.h"

      // start compiling b.h
      #include "a.h"

         // compilation of a.h skipped because it's guarded

      // resume compiling b.h
      class B { A* a };        // <--- ERROR, A is undeclared 


자 위 코드를 차례대로 따라가 보면, a.cpp를 컴파일 시작하면 우선 a.h 헤더 파일이 선두이므로 a.h를 컴파일 하기 시작하는데 a.h의 선두에는 b.h가 선언되므로 b.h를 컴파일 하려고 하는데 다시 b.h의 선두에 a.h가 선언 되므로 a.h를 컴파일 하려고 하자, include guard가 되어버린 경우, a.h를 스킵하고 지나가버리게 된다. 고로 A라는 클래스를 알 수 없다는 에러가 터진다. 이것이 circular include의 문제점이다. 


이것을 방지하려면 앞서 살펴본 원칙대로 forward declare를 하면 된다. 왜냐면 reference 타입만 사용된 경우에는 forward declare를 통해 이런 circular include의 연결 고리를 끊어줄 수 있기 때문이다.


고로 만약 두 헤더 파일이 서로의 클래스를 직접적인 객체로 사용하는 경우에는 이 circular include 문제를 해결할 수 가 없다. 


1
2
3
4
5
6
7
8
// a.h (guarded)

#include "b.h"

class A
{
  B b;   // B is an object, can't be forward declared
};


1
2
3
4
5
6
7
8
// b.h (guarded)

#include "a.h"

class B
{
  A a;   // A is an object, can't be forward declared
};


헌데 잘 보면 이러한 경우는 구조 자체가 설계가 잘못 된 경우다. 이런것은 infinite recursion을 발생시키게 된다. 고로 올바른 형태는 다른 클래스의 포인터를 사용하는 것이 올바르다. Forward declare가 바로 이런 이유로 사용되는 것이다.



 7> 인라인 함수 


1
2
3
4
5
6
7
8
9
10
class B
{
public:
  void Func(const A& a)   // parameter, so forward declare is okay
  {
    a.DoSomething();      // but now that we've dereferenced it, it
                          //  becomes an #include dependency
               // = we now have a potential circular inclusion
  }
};


자, 위의 코드를 보면 인라인 함수가 circular inclusion 문제를 야기할 수 있음을 알 수 있다. 입력 변수로 선언된 시점 까지는 forward declare가 가능하지만 DoSomething()이라는 함수를 호출하는 순간 객체가 구현되므로 반드시 #include를 해야하는 문제가 발생한다.


고로 이것을 고치려면 아래와 같이 바꾸면 된다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// b.h  (assume its guarded)

//------------------
class A;  // forward declared dependency

//------------------
class B
{
public:
  void Func(const A& a);  // okay, A is forward declared
};

//------------------
#include "a.h"        // A is now an include dependency

inline void B::Func(const A& a)
{
  a.DoSomething();    // okay!  a.h has been included
}


단순히 봐서 이게 뭐가 다르지 라는 생각이 들것이다. 헤더 파일을 인라인 함수 앞에 가져다 놓고 인라인 함수를 클래스 선언에서 분리했다는 점 외에는 차이점이 없다. 그런데... 이것은 완벽하게 안전하다. 왜냐면 Class B가 완전히 정의된 후에 a.h가 include 되었기 떄문이란다. 흠.


그런데 헤더 파일 안에 함수가 구현되어 있는게 보기 싫은 분들은 다음과 같이 해도 된다. 


1
2
3
4
5
6
7
// b.h

    // blah blah

class B { /* blah blah */ };

#include "b_inline.h"  // or I sometimes use "b.hpp" 


1
2
3
4
5
6
7
8
9
10
11
// b_inline.h (or b.hpp -- whatever)

#include "a.h"
#include "b.h"  // not necessary, but harmless
                //  you can do this to make this "feel" like a source
                //  file, even though it isn't

inline void B::Func(const A& a)
{
  a.DoSomething();
}


인라인 함수 부분은 .hpp 파일로 분리해 놨다. 이제 보니 hpp가 왜 hpp인지 알겠다. 헤더 파일이지만 코드를 구현하고 있기 때문에 pp를 붙여 놓은 것이다.



 8> Forward Declaring Templates 


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// a.h

// included dependencies
#include "b.h"

// the class template
template <typename T>
class Tem
{
 /*...*/
  B b;
};

// class most commonly used with 'int'
typedef Tem<int> A;  // typedef'd as 'A' 


1
2
3
4
5
6
7
8
9
10
11
// b.h

// forward declared dependencies
class A;  // error!

// the class
class B
{
 /* ... */
  A* ptr;
};


사실 forward declare은 굉장히 간단한 것이다. 헌데 만약 그것이 template class에 대한 것이라면 문제가 좀 복잡해 진다. 위의 예를 보면 알수 있듯이 template 클래스 Tem이라는 것이 B 클래스를 사용하고 있다. 고로 b.h를 반드시 include 해야 하고, typedef을 통해 A라는 클래스를 Tem<int> 클래스로 정의했다.


그리고 b.h에서 A 클래스를 forward declare 하려고 하면 에러가 빡 터진다.. 왜?


왜냐면 A는 클래스가 이니라 사실 typedef이기 때문이다. 문제는 a.h를 include 할 수 없는데 왜냐면 circular inclusion 문제가 발생하기 때문이다. 그러면 어떻게 해결하느냐?


a.h에서 해준 것 처럼 똑같이 어떤 템플릿 클래스를 typedef 했는지 고대로 알려주면 A라는 클래스를 forward declare 가능하다. 즉, 아래와 같이 친절하게 알려주어야 한다.


1
2
template <typename T> class Tem;  // forward declare our template
typedef Tem<int> A;               // then typedef 'A' 


근데 이건 단순히 class A라는 한 줄에 비해 참 보기 싫다. 그래서 실용적인 해결 방법은 이러한 템플릿 클래스의 typedef를 따로 헤도로 선언하여 포함시키는 것이다. 아래와 같이...


1
2
3
4
5
6
7
8
9
10
//a.h

#include "b.h"

template <typename T>
class Tem
{
 /*...*/
  B b;
};


1
2
3
4
//a_fwd.h

template <typename T> class Tem;
typedef Tem<int> A;


1
2
3
4
5
6
7
8
9
//b.h

#include "a_fwd.h"

class B
{
 /*...*/
  A* ptr;
};


이런식으로 템플릿 클래스의 typedef 또한 forward declare가 가능하다는 점~






posted by 대갈장군
prev 1 next