티스토리 뷰

반응형

파일 사이의 컴파일 의존성을 최대로 줄이자.

클래스 하나만 추가해서 빌드를 했더니 건들지도 않은 부분도 컴파일되고 다시 링크된다.

C++가 인터페이스와 구현을 깔끔하게 분리하지 못하기 때문입니다.
C++의 클래스 정의는 클래스 인터페이스만 지정하는 것이 아니라 구현 세부사항까지 지정하고 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person 
{
public:
    Person(const std::string&name, const Date& birthday, const Address& addr);
    std::string name() const;
    std::string birthDate() const;
    std::string address() const;
private:
    std::string theName;   // 구현 세부사항
    Date theBirthDate;   // 구현 세부사항
    Address theAddress;   // 구현 세부사항
};
 
 
 
 
cs

위의 코드만 가지고는 컴파일이 불가능합니다. 따라서, 세 가지의 선언이 필요합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <string>
#include "date.h"
#include "address.h"
//#include 문은 Person을 정의한 파일과 헤더 파일들 사이에 컴파일 의존성을 만듭니다.
namespace std {
    class string;   // 전방선언(x)
}
class Date;
class Address;
class Person 
{
public:
    Person(const std::string& name, const Date& birthday, const Address& addr);
    std::string name() const;
    std::string birthDate() const;
    std::string address() const;
};
 
cs

 

문제점

1. string은 사실 클래스가 아니라 typedef로 정의한 타입 동의어(basic_string <char>를 typedef 한 것)입니다.
string에 대한 전방 선언은 틀렸습니다, 전방 선언을 하려면 템플릿을 추가로 끌고 들어와야 하기 때문에 더 복잡해집니다.

2. 컴파일러가 컴파일 도중에 객체들의 크기를 전부 알아야 한다는데 있습니다.
int main()
{
  int x; // int 하나를 정의
  Person p(params); // Person 하나를 정의
}

컴파일러는 x의 정의문을 만나면 int 하나를 담을 공간을 할당합니다.
p의 정의문을 만나면 Person 하나를 담을 공간을 할당해야 하지만

Person 클래스의 크기를 알려면 Person 클래스의 구현 세부사항을 알아야 합니다.
*스몰토크 및 자바의 경우 객체가 정의될 때 컴파일러가 그 객체의 포인터를 담을 공간만 할당함

int main()
{
  int x; // int 하나를 정의
  Person *p; // Person 하나에 대한 포인터 하나를 정의
}

C++에서 '포인터 뒤에 실제 객체 구현부 숨기기'

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <string>
#include <memory>
class PersonImpl; 
class Date; 
class Address;
class Person 
{
public:
    Person(const std::string& name, const Date& birthday, const Address& addr);
    std::string name() const;
    std::string birthDate() const;
    std::string address() const;
private:
    std::tr1::shared_ptr<PersonImpl> pImpl;   // 구현 클래스 객체에 대한 포인터 std::tr1::shared_ptr
};
cs

Person 클래스를 구현해놓은 PersonImpl 클래스는 맘대로 고칠 수 있고 Person을 사용하는 쪽에서는 컴파일을 다시 할 필요가 없습니다.

정의부에 대한 의존성을 선언부에 대한 의존성으로 바꾸면 됩니다.

정리

객체 참조자 및 포인터로 충분한 경우에는 객체를 직접 쓰지 않습니다.

할 수 있으면 클래스 정의 대신 클래스 선언에 최대한 의존하도록 만든다.

어떤 클래스를 사용하는 함수를 선언할 때는 그 클래스의 정의가 필요 없습니다.

 

여러 가지 방법

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
class Date; // 클래스 선언
 
Date today();
 
void clearAppointments(Date d); // Date 클래스의 정의를 가져오지 않아도 됩니다.
 
l. 선언부와 정의부에 대해 별도의 헤더 파일을 제공됩니다.
 
라이브러리 사용자는 전방선언 대신에 헤더파일을 항상 #include 해야합니다.
 
#include "datefwd.h"
 
Date today();
 
void clearAppointments(Date d);
 
C++에서 지원하는 iostream 관련 함수 및 클래스들의 선언부만으로 구성된 헤더입니다.
 
각각의 정의부는 일정한 분류에 따라 여러 개의 헤더에 나뉘어 있는데,<sstream>,<streambuf>,<fstream>,<iostream> 등입니다.
 
pimpl 관용구를 사용하는 Person 같은 클래스를 가르켜 핸들클래스라고 합니다.
 
핸들 클래스에서 어떤 함수를 호출하게 되어있다면 구현 클래스 쪽으로 그 함수 호출을 전달해서 구현클래스가 실제 작업을 수행하게 만들 수 있습니다.
 
 
Ex)
 
#include "Person.h"
#include "PersonImpl.h"
class Person
{
public:
    Person::Person(const std::string& name, const Date& birthday, const Address& addr) 
        : pImpl(nePersonImpl(name, birthday, addrr)) 
    {
 
    }
 
    std::string Person::name() const
    {
        return pImpl->name();
    }
};
 
핸들 클래스 방법 대신 추상 기본 클래스, 인터페이스 클래스로 만들어도 됩니다.
파생 클래스만 만들 수 있게 합니다.
데이터 멤버도 없고, 생성자나 가상 소멸자도 없는 순수 가상함수만 만듭니다.
 
class Person 
{
public:
    virtual ~Person();
    virtual std::string name() const = 0;
    virtual std::string birthDate() const = 0;
    virtual std::string address() const = 0;
}
 
인터페이스 클래스를 사용하기 위해서 객체 생성 수단이 필요한데 파생 클래스의 생성자 역할 대신하는
팩토리 함수 혹은 가상 생성자를 만들어 이것을 호출합니다.
 
팩토리 함수의 역할은 주어진 인터페이스 클래스의 인터페이스를 지원하는 객체를 동적으로 할당한 후 그 객체의 포인터를 반환 하는 것 입니다.
 
class Person 
{
public:
    static std::tr1::shared_ptr<Person> create(const std::string& name, const Date& birthday, const Addressaddr);
};
 
사용자 쪽에서는 아래와 같이 하면 됩니다.
 
void main()
{
    std::string name;
    Date dateOfBirth;
    Address address;
    
    // Person 인터페이스를 지원하는 객체 한 개를 생성
    std::tr1::shared_ptr<Person> pp(Person::create(name, dateOfBirth, address));
    std::cout << pp->name() << "was born on" << pp->birthDate() << " and now lives at " << pp->address();
}
 
인터페이스 클래스를 구현하는 용도로 많이 쓰이는 매커니즘
Person 클래스로부터 상속받은 RealPerson이라는 구체 클래스가 있다면, 이 클래스는 자신 상속받은 순수 가상함수에 대한 구현부를 제공하는 식입니다.
 
class RealPerson : public Person 
{
public:
    RealPerson(const std::string& name, const Date& birthday, const Address& addr)
        : theName(name), theBirthDate(birthday), theAddress(addr)  
    {
    }
    virtual ~RealPerson() 
    {
    }
 
    std::string name() const;
    std::string birthDate() const;
    std::string address() const;
 
private:
    std::string theName;
    Date theBirthDate;
    Address theAddress;
};
 
std::tr1::shared_ptr<Person> Person::create(const std::string& name, const Date& birthday, const Address& addr)
{
    return std::tr1::shared_ptr<Person>(new RealPerson(name, birthday, addr));
}
 
create 함수를 사용자가 원하는 대로 다른 타입의 파생 클래스 객체를 생성할 수 있게 만들면 됩니다.
 
매개변수를 더 받도록 해서 매개변수의 값에 따라 타입을 다르게 하는 등
인터페이스 클래스로부터 인터페이스 명세를 물려받게 만든 후, 인터페이스에 들어있는 가상함수를 구현하는 것입니다.
 
 
 
 
 
 
 
 
cs

 

총평

컴파일 의존성을 최소화하는 작업의 배경이 되는 가장 기본적인 아이디어는 ‘정의’ 대신에 ‘선언’에 의존하게 만들자는 것입니다. 이 아이디어에 기반한 두 가지 접근 방법은 핸들 클래스와 인터페이스 클래스입니다

라이브러리 헤더는 그 자체로 모든 것을 갖추어야 하며 선언 부만 갖고 있는 형태여야 합니다. 이 규칙은 템플릿이 쓰이거나 쓰이지 않거나 동일하게 적용합시다.

 

 

 

 

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/03   »
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
글 보관함