postgame

java에서 c++로 넘어왔을때 헤더파일로 관리 해주는데 익숙치 않아서 고생했던 기억이 있네요.
헤더파일을 어떻게 관리하냐에 따라 컴파일 속도가 어마어마하게 차이가 납니다.

누구는 최신 cpu를 써서 하드웨어 성능을 올리거나,incredibuild(분산 컴파일 프로그램. 상용임)을
써서 컴파일 속도를 개선하는 방법을 이야기합니다만,
결과적으론 개발자가 헤더파일을 어떻게 관리할껀지 잘 판단한다면  컴파일 속도로
스트레스 받는 일은 줄일 수 있습니다.

EC++에서는 여기에 대해서는
파일간의 컴파일 의존성을 최소화하라. (2판 항목 34, 3판 항목 31)
라고 이야기를 합니다.

일단, 실무에서 많이 생기는 문제를 예를 들어봅시다.
  1. class AAA
  2. {
  3. public:
  4.     int a;
  5. };

가령 위와 같은 AAA.h가 있다고 하겠습니다.
이 클래스를 다른 BBB.h의 BBB가 사용한다고 칩시다.

  1. #include "AAA.h"
  2. class BBB
  3. {
  4. public:
  5.     AAA a;    <<---여기
  6. };

BBB는 AAA를 가지고 있기 때문에 AAA.h헤더를 가지고 있어야 하고,
고로 AAA가 수정된다면, BBB는 항상 함께 컴파일되어야 합니다.
이정도 의존성이야 넘어간다고 칩시다.

하지만, BBB에서 더이상 AAA를 사용하지 않아서 삭제하게 되었다면..
  1. #include "AAA.h" <<-- !!!
  2. class BBB
  3. {
  4. public:
  5.     //AAA a; <---어랏 이건 지웠는데..
  6. };
문제는 여기서 부터입니다.
사용하지 않는 클래스는 지웠는데 헤더는 잊어버리고 삭제를 안한 경우,
BBB는 AAA랑 관계도 없는데, AAA가 수정될때마다 계속 다시 컴파일 되어야 합니다.

이런 바보같은 경우는 없을 것 같으면서도 실무에서는 흔하게 발생합니다.

특히 위처럼
헤더파일안에 헤더파일이 들어가게 된다면
추적해서 파악하기가 무척 어렵습니다.

전에 같이 개발하신 분중에
그냥 무조건 헤더파일을 include하고 보는 분이 있었는데요.

프로젝트 초에는 별다른 속도차를 느끼지 못하겠지만,
후반으로 갈수록 무한 짜증 컴파일 속도를 경험하게 됩니다.

여기에 대한 해결책에 대해
가장 심플한 방법이 있습니다.

그냥 헤더파일안에 헤더파일을 안넣으면 됩니다.

  1. #include "AAA.h"
  2. #include "BBB.h"
  3.  
  4. int main()
  5. {
AAA, BBB를 사용하는 cpp에 그냥 위처럼 순서를 지켜서 넣어버리는 방법입니다.
실제로 gpg답변을 보면 위처럼 해결해 사용하시는 분들이 많은 것 같습니다.
저도 왠만하면 헤더파일안에 헤더를 넣지는 않고요.

하지만,
위처럼 하더라도, 의존성 문제는 그대로 안고 있을 뿐더러,
cpp안에 헤더파일을 쌓아야 하는 양이
너무 많아지는 문제가 있습니다.

그리고 거의 수정되지 않는 헤더파일은 그냥 헤더파일 안에 포함 시키는게 나을 수도 있고요.

여기에 대한 해결책은 ..


  1. class AAA;
  2. class BBB
  3. {
  4. public:
  5.     AAA *a;
  6. };

간단하죠. 포인터만 들고 있고, 전방위 선언해버리는 방법입니다.

그냥 포인터만 들고 있으면 그러니, 스마트 포인터를 쓰면 더 좋겠군요.

EC++에서는 아예 관용적으로
포인터 안에 구현을 숨겨버리는 방법을 소개합니다.

pimpl 관용구(pointer to implementaion)인데요.
뭔가 있어보이지만, 별건 없습니다.

가령,
  1. #include <iostream>
  2. #include "Person.h"
  3.  
  4. int main()
  5. {
  6.  
  7.     Person p("DUDU");
  8.     std::string g = p.name();
  9.  
  10.     std::cout<<g.c_str()<<std::endl;
  11.  
  12.     getchar();
  13.  
  14. }
란 Person이란 클래스를 쓰는 프로그램이 있다고 합시다.
Person은 이름을 입력받는 생성자랑 가져오는 name함수만 있구요.

여기 메인 함수는 Person이 수정되면 함께 수정되서 컴파일 되게 됩니다.

하지만, Person이 포인터만 들고 있으면 어떨까요?
  1. class PersonImpl;
  2. class Person
  3. {
  4. public:
  5.     PersonImpl *pImpl;
  6.  
  7.     Person(std::string _name);
  8.     std::string name() const;
  9. };
이러고..
pImpl은 실제 구현부를 가지고 있습니다.

  1. class PersonImpl
  2. {
  3. public:
  4.     std::string theName;
  5.     PersonImpl(std::string name):theName(name){};
  6.     std::string name() const { return theName; };
  7. };

그러고, 진짜 Person은
그냥 pImpl을 생성하고, 연결만 해주는 역할을 합니다.

  1. #include <iostream>
  2. #include "Person.h"
  3. #include "PersonImpl.h"
  4.  
  5. Person::Person(std::string _name) :pImpl(new PersonImpl(_name))
  6. {
  7. }
  8.  
  9. std::string Person::name() const
  10. {
  11.     return pImpl->name();
  12. }
  13.  
끗.
여기선 new에 대한 delete를 빼먹었는데..
기왕이면 shared_ptr이 좋겠죠?

pImp관용구에선, Person같은 클래스를 핸들클래스(handle class)라고 한다는군요.,

요렇게 되면,
main함수는 person안에 실제 구현인 pImpl이 수정되도 아무 상관이 없게 됬습니다..

하지만, 뭔가 멋져보이긴 한데..
실무에서 쓰기가 좀 그래보이죠?

언제 헤더파일 나누고,언제 pmpl를 만들고 있습니까, 바빠죽겠는데..
그래서..
상속을 통한 방법을 활용있습니다.

Person.h에서

  1. class Person
  2. {
  3. public:
  4.     virtual ~Person(){};
  5.     virtual std::string name() const = 0;
  6.  
  7.     static Person* create(std::string name);
  8. };
우선 Person을 추상 클래스로 바꿔놓습니다.
인터페이스로만 쓰겠다는거죠.
단, static으로 된 팩토리 함수 create를 유의해서 보세요.
Person에는 구현이 없습니다.

그리고 실제 구현이 들어갈 RealPerson의 헤더를 만듭니다.
  1. #include <iostream>
  2. #include "Person.h"
  3. class RealPerson : public Person
  4. {
  5. public:
  6.     RealPerson(std::string name) : theName(name){};
  7.     ~RealPerson() {};
  8.  
  9.     std::string name() const
  10.     {
  11.         return theName;
  12.     }
  13.  
  14. private:
  15.     std::string theName;
  16. };
전방위 선언으로는 상속을 할 수 없음으로,
Person의 헤더파일을 include해줍니다.

  1. #include "RealPerson.h"
  2.  
  3. Person* Person::create(std::string name)
  4. {
  5.     return (new RealPerson(name));
  6. }
그리고, RealPerson.cpp에
Person의 create를 만들어줍니다.

구현만 어디에 되어있으면 굳이 cpp파일 명을 따지지 않으니
자식 클래스 선언한 뒤에 넣어도 아무 문제가 없죠?

  1. #include <iostream>
  2. #include "Person.h"
  3.  
  4. int main()
  5. {
  6.  
  7.     Person *p = Person::create("DUDU");
  8.     std::string g = p->name();
  9.  
  10.     std::cout<<g.c_str()<<std::endl;
  11.  
  12.     getchar();
  13.  
  14. }
그리고, 이제 직접 new생성이 아닌
create를 사용하여 Preson을 만들어 줍니다. 실제로는 RealPerson를 만들겠구요
(마찬가지 delete해주는게 빠졌는데, 역시 shared_ptr이 좋겠죠?)

여기의 main 함수도 Person.h만 include한데 주의하세요.
이 main함수도 Person의 구현이 수정됬을 때 pImp처럼 상관이 없게 되었습니다.
의존성이 깨졌네요. 짝짝짝

객체지향 원칙인 인터페이스 분리 원칙에도 잘 부합되는 방법이구요.
팩토리 함수인 create를 조금더 유동적으로 만들면
더 써먹을 데가 많을 것 같습니다.

여기까진 EC++에 나왔던 방법입니다만,
유나이티 빌드란 컴파일 속도를 증가시키는 아주 독특한 방법도 있습니다.
http://whonz.egloos.com/2243326

역시 개발의 세계는 무궁무진 하죠?
Posted by 중원_