U E D R , A S I H C RSS

More EffectiveC++/Miscellany

1. Item 32: Program in the Future tense

  • Item 32: 미래를 대비하는 프로그램
(작성자주:제목은 직역보다, 전체 내용으로 결정했다.)

사물은 변화한다.

소프트 웨어 개발자라면, 우린 아마 많은것을 알 필요는 없다. 하지만 변하는 것은 인지해야 한다. 우리는 무엇이 변할건가, 어떻게 변할건가, 언제 변화가 일어나는가, 왜 거기에서 변화가 일어나는가 이런것 따위는 알필요가 없다. 그렇지만 우린 알아야 한다. 변화한다. 라는 점 말이다. (작성자주:개인적인 의견으로 의역한 단락, 혹시나 관심있으면 의견좀 내주세요.)

원문:As software developers, we may not know much, but we do know that things will change. We don't necessarily know what will change, how the changes will be brought about, when the changes will occur, or why they will take place, but we do know this: things will change.

좋은 소프트웨어는 변화를 잘 수용한다. 새로운 기능을 수용하고, 새로운 플랫폼에 잘 적용되고, 새로운 요구를 잘 받아 들이며, 새로운 입력을 잘 잡는다. 이런 소프트웨어는 유연하고, 강하고, 신뢰성있고, 돌발 상황(사고)에 의해 죽지 않는다. 이런 소프트웨어는 미래에 필요한 요소를 예상하고, 오늘날 구현시에 포함시키는 프로그래머들에 의해서 디자인된다. 이러한 종류의 소프트웨어는-우아하게 변화하는 소프트웨어- program in the future tens(매래의 프로그램:이하 영문 직접 사용)을 감안하는 사람들이 작성한다.

program in future tense는, 변화의 수용하고, 준비한다. 라이브러에 추가될 새로운 함수, 앞으로 일어날 새로운 오버로딩(overloading)을 알고, 잠재적으로 모호성을 가진 함수들의 결과를 예측한다. 새로운 클래스가 상속 계층에 추가될 것을 알고, 이러한 가능성에 대하여 준비한다. 새로운 어플리케이션에서 코드가 쓰이고, 그래서 새로운 목적으로 함수가 호출되고, 그런 함수들이 정확히 동작을 유지한다. 프로그래머들이 유지 보수를 할때, 일반적으로 원래의 개발자의 영역이 아닌, 유지 보수의 몫을 안다. 그러므로, 다른 사람에 의해서 소프트웨어는 이해, 수정, 발전의 관점에서 구현하고 디자인된다.

이런 좋은 소프트웨어를 만들기 위한 방법으로, 주석이나, 기타 다른 문서 대신에 C++ 내부에 디자인으로 구속해 버리는 것이다. 예를들자면 만약 클래스가 결코 다른 클래스로 유도되지를 원치 않을때, 단시 주석을 헤더 파일에 넣는 것이 아니라, 유도를 방지하기 위하여 C++의 문법을 이용한 기술로 구속 시킨다.;이에 대한 방법은 Item 26에 언급되었다. 만약 클래스가 모든 인스턴스를 Heap영역에 생성시키고자 할때, 클라이언트에게 말(문서)로 전달하는 것이 아니라. Item 27과 같은 접근으로 제한 시켜 버릴 수 있다. 만약 클래스에 대하여 복사와 할당을 막을려고 할때는, 복사 생성자와 할당(assignment) 연산자를 사역(private)으로 만들어 버려라. C++은 훌륭한 힘과, 유연성, 표현성을 제공한다. 이러한 언어의 특징들을 당신의 프로그래밍에서 디자인의 정책을 위해서 사용하라.


DeleteMe 모호

"변화한다.", 험난한 소프트웨어의 발전에 잘 견디는 클래스를 작성하라. (원문:Given that things will change, writeclasses that can withstand the rough-and-tumble world of software evolution.) "demand-paged"의 가상 함수를 피하라. 다른 이가 만들어 놓지 않으면, 너도 만들 방법이 없는 그런 경우를 피하라.(모호, 원문:Avoid "demand-paged" virtual functions, whereby you make no functions virtual unless somebody comes along and demands that you do it) 대신에 함수의 meaning을 결정하고, 유도된 클래스에서 새롭게 정의할 것인지 판단하라. 그렇게 되면, 가상(virtual)으로 선언해라, 어떤 이라도 재정의 못할지라도 말이다. 그렇지 않다면, 비가상(nonvirtual)으로 선언해라, 그리고 차후에 그것을 바꾸어라 왜냐하면 그것은 다른사람을 편하게 하기 때문이다.;전체 클래스의 목적에서 변화를 유지하는지 확신을 해라.

모든 클래스에서 할당(assignment), 복사를 잡아라. "비록 아무것도 하지 않는 것"이라도 말이다. 왜냐하면 그것들이 지금 할수 없는건 미래에도 할수 없다는 의미이다. 만약 이러한 함수들이 구현하기에 어렵게 한다면, 그것을 private로 선언하라. 미래에도 동작시키게 하지 않다는 의미다. 컴파얼러가 만들어내는 함수에 대한 모호한 호출을 할리가 없다. (기본 할당 생성자나 기본 복사 생성자가 종종 발생되는 것처럼)

원리를 구현하기 위해 특이하게 하지 마라.:연산자와 함수를 자연스럽고 명시적인 문법으로 제공하라. built-in(기본 자료) 형으로 구현하라:의심될때는 int로 하라

어떤이가 무언가를 할수있다는것 알게되면 그들을 그것을 할것이다. 그들은 예외를 던질 것이다. 그들은 그들 스스로에게 객체를 할당할 것이다. 그들은 값을 제공하기전에 객체를 사용할 것이다. 그들은 객체를 제공하겠지만, 결코 사용하지 않는다. 그들은 커다란 값을 제공할 것이다. 그들은 아주 작은 값을 제공할 것이다. 그들은 null 값을 제공할 것이다. 일반적으로 만약 컴파일이 되면 어떤이가 그것을 할것이다. 결과적으로 당신의 클래스를 정확히 사용하는건 쉽게, 이상하게 사용하는건 어렵게 만들어라. 클라이언트가 실수를 하도록 해라 그리고 당신의 클래스들이 그러한 에러들을 방지하고, 찾고, 수정할수 있게 만들어라. (예를들어 Item 33 참고)

이식성 있는 코드를 만들어라. 이식성 있는 프로그램의 제작은 그렇지 않은 경우보다 매우 어려운게 아니다. 그리고 눈에 보일 만큼 성능면에서 이식성 없는 코드를 고집해야 하는 경우는 희귀하다.(Item 16참고) 특정한 하드웨어를 위한 프로그램의 디자인에서도, 얼마 안있어 하드웨어 성능이 동일한 수준의 성능을 가져다 주므로, 이식성 있도록 만들어라. 이식성 있는 코드의 작성은 플랫폼 간의 변환에 쉽고, 당신의 클라라이언트 기반을 공고히 하고, 오픈 시스템의 지원에 관하여 좋다. 만약, 한 OS에서 성능이 실패해도, 쉽게 복구할수 있다.

당신의 코드를 변화가 필요할때, 그 효과를 지역화(지역화:localized) 시키도록 디자인 해라. 가능한한 캡슐화 하여라:구체적인 구현은 private 하라. 광범위하게 적용해야 할곳이 있다면 이름없는(unamed) namespace나, file-static객체 나 함수(Item 31참고)를 사용하라. 가상 기초 클래스가 주도하는 디자인은 피하라. 왜냐하면 그러한 클래스는 그들로 부터 유도된 모든 클래스가 초기화 해야만 한다. - 그들이 직접적으로 유도되지 않은 경우도(Item 4참고) if-than-else을 개단식으로 사용한 RTTI 기반의 디자인을 피하라.(Item 31참고) 항상 클래스의 계층은 변화한다. 각 코드들은 업데이트 되어야만 한다. 그리고 만약 하나를 읽어 버린다면, 당신의 컴파일러로 부터 아무런 warning를 받을수 없을 것이다.

이와 같은 내용들을 아무리 반복해서 말하곤 하지만, 대부분의 프로그래머들은 현재의 시류를 그대로 고집한다. 훌륭한 안목의 C++ 전문가가 말하는 충고에 관해서 생각해라.

  • 당신은 B*가 가리키고 있는 D를 제거할때 가상 파괴자를 필요로 한다.

여기에서 B는 D의 기초 클래스이다. 다른 말로 하자면 이 작성자는 만약 다음과 같은 경우에 B는 가상 파괴자(virtual destroctor)가 필요 없어 보인다.

~cpp 
class B { ... };	// 가상 파괴자가 없다.
class D: public B { ... } 
    B *pb = new D;

그렇지만 다음과 같은 구문이 더해지면, 생각이 바뀔것이다.


~cpp 
delete pb;	// 자, 당신은 B에서 가상 생성자가 필요하다. 안그런가?

그 의미는 클라이언트 코드에 대하여 약간의 변화가 -delete문-결과적으로 클래스 B의 정의까지 변화해야 하는 필요성을 보여준다. 그런한 상황이 발생하면, B의 클라이언트들은 모두 재 컴파일 해야 한다. 아까, 이 필자의 충고를 따르면, 확장 코드에 대한 클라이언트의 라이브러리도 재 컴파일, 재 연결해야 한다. 이는 소프트웨어 디자인에 효과를 미틴다.

DeleteMe 전체적으로 모호가 아니다, 뒤에 내용을 봐야 앞에 내용이 이해가 감.

같은 주제로, 또 다른 필자의 글을 보자.

  • 만약 public base class가 가상 파괴자를 가지고 있지 않다면, 유도된 클래스나, 유도된 클래스의 멤버들이 파괴자를 가지고 있지 않다.

다른 말로 하면 이런거다.


~cpp 
class String {
public:
    ~string();
};
class B { ... }
그렇지만 새로운 클래스가 B로부터 유도되면 바뀌어야 한다.


~cpp 
class D: public B{
    string name;
};

다시, B에 작은 변화는 아마 클라이언트의 부가적인 재 컴파일과 재링크를 요구한다. 그렇지만 소프트웨어의 작은 변화는 시스템에 작은 충격을 줄것이다. 이러한 디자인은 테스트로 실패이다.

같은 필자는 이렇게 쓴다.

  • 만약 다중 상속 상태에서 어떠한 파괴자가 있다면, 모든 기본 클래스가 아마 가상 파괴자(virtual destructor)가 되어야 할것이다.
이렇게 반복에서 말하는거 같이 현재의 시류를 생각하는걸 주시하라. 클라이언트가 지금 늘어나고 있는 의견들에 대하여 어떻게 해야 하는가? 어떤 클래스 멤버가 지금 파괴자를 가지고 있는가? 계층상에 어떤 클래크가 지금 파괴자를 가지는가?

미래의 시류로 생각하는 관점은 완전히 다르다. 지금 어떻게 클래스를 사용하느냐를 묻는것 대신에, 어떻게 클래스를 디자인 하느냐를 묻는다. 미래 지향적 생각으로는 이렇게 말한다. 만약 기초 클래스로 사용된 클래스가 디자인 된다면 그 클래스는 가상 파괴자를 가져야 한다. 그러한 클래스는 지금과 미래 모두 정확히 동작해야 한다. 그리고 그들오 부터 클래스들이 파생될때 다른 라이브러리의 클래스에게 영향을 끼쳐서는 안된다. ( 최소한, 파괴자로 인한 논란 만큼, 영향이 없어야 한다. 추가적인 변화가 클래스에 필요하면 다른 클라이언트들오 아마 영향을 받을 것이다.)

상업용 클래스 라이브러리(C++표준 라이브러리 상의 string 스펙의 날짜를 앞당기려는 회사)는 가상 파괴자를 가지고 있지 않은 sting클래스를 포함한다. 그 벤더의 설명은?

  • 우리는 가상 파괴자를 만들지 않는다. 왜냐하면, String가 vtbl을 가지기를 원하지 않기 때문이다. 우리는 String*를 가지게할 의도는 없다. 그래서 이는 문제가 되지 않는다. 우리는 이것이 수반하는 어려움에 대하여 생각하지 않는다.
이것이 현재나 미래의 시류를 생각하는 것인가?

확실히 vtbl 문제는 합법적인 접근이다. (Item 24참고) 대다수 String클래스의 구현에서 오직 하나의 char*를 각각의 String 객체가 가지고 있다. 그래서 각 String객체에 추가되는 vptr도 두배의 양을 차지한다. 허용하지 않으려는 이유는 이해하기 쉽다. String같은 클래스를 무겁게 사용하면 눈에 띠는 성능 저하가 있다. 앞서 언급한 경우 클래스당 성능 저하는 약 20%정도를 가지고 온다. (Item 16참고)

문자열 객체에 대한 메모리의 할당은-문자의 값을 가지고 있기 위해 필요로하는 heap메모리까지 감안해서-일반적으로 char*이 차지하는 양에 비하여 훨씬 크다. 이러한 관점에서, vtpr에 의한 오버헤드(overhead)는 미미하다. 그럼에도 불구하고, 그것은 할만한(합법적인,올바른) 고민이다. (확실히 ISO/ANSI 포준 단체에서는 그러한 관점으로 생각한다. 그래서 표준 strnig 형은 비 가상 파괴자(nonvirtual destructor) 이다.)

어떤 것이 더 많은 문제를 일으키는 것으로, 밴더들의 주목을 받고 있을까? "우리는 String*을 사용하는 목적을 가지지 않는다. 그래서 이는 별 문제가 되지 않는다." 그건 아마 사실일 것이다. 하지만 그들의 String클래스는 수많은 개발자들이 사용가능한 것이다. 수많은 개발자들이 C++의 수준이 제각각이다. 이러한 개발자들이 String상에서의 비가상 파괴자(no virtual destructor)를 이해할까? 그들이 비가상 파괴자를 가진 String때문에 String으로 유도된 새로운 클래스가 모험 비슷한 것을 알고 있을까? 이런 벤더들은 그들의 클라이언트들이 가상 파괴자가 없는 상태에서 String*를 통하여 삭제가 올바르게 작동하지 않고, RTTI와 String에 대한 참조가 아마 부정확한 정보를 반환한다는걸 확신시킬까? 이 클래스가 정확히 쓰기 쉬운 클래스일까? 부정확하게 쓰기 어려운 클래스일까?

이 벤더는 물론 String클래스에 관한 유도해서는 안되도록 디자인 된 문서들을 제공할 것이다. 하지만, 프로그래머들이 문서를 보는 도중에 그 부분을 놓쳤다면 어떻게 하겠는가?

대안으로 C++을 사용할때 유도를 제한해 버리는 것이다. Item 26에서 어떻게 객체를 heap에 만들거고 auto_ptr객체로 heap객체를 조정하는 방법에 관해서 언급하였다. String을 위한 인터페이스 생성은 아마 독특하고 불편한 다음과 같은 문법 을 요구한다.


~cpp 
auto_ptr<String> ps(String::makeString("Future tense C++"));
...	// 이제 해당 객체는 포인터처럼 다룬다. 하지만 delete를 부르지 말아야 한다.

을 이것 대신에


~cpp 
String s("Future tense C++");

하지만 정확하지 않게 동작하는 유도된 클래스의 사용을 억제하는 것은 문법적으로 상당히 불편함을 낳는다. (String에서 이런 문법적으로 불편한 면이 그리 강조되지 않다. 그렇지만 다른 클래스에서 이러한 문법적인 불편함을 따지는 면이 중요하다.)

물론, 필요하다면 현재 감안하는 생각으로 접근한다. 당신이 개발중인 소프트웨어는 현재의 컴파일러에서 동작해야만 한다.;당신은 최신의 언어가 해당 기능을 구현할때까지 기다리지 못한다. 당신의 현재 가지고 있는 언어에서 동작해야 하고. 그래서 당신의 클라이언트에서 사용 가능해야 한다.;당신의 고객에게 그들의 시스템을 업그레이드 하거나, 수행 환경을(operating environment) 바꾸게 하지는 못할것이다. 그건은 지금 수행함을 보증해야 한다.;좀더 작은, 좀더 빠른 프로그램에 대한 약속은 라이프 사이클을 줄이고, 고객에게 기대감을 부풀릴 것이다. 그리고 당신이 만드는 프로그램은 작동해야만 한다. 이는 종종 "최신의 과거"를 만들어 버린다. 이는 중요한 속박이다. 당신은 이를 무시할수 없다.

  • 어떤 부분이 현재사용할수 없더라도, 완전한 클래스를 제공하라. 새로운 요구가 당신의 클래스를 만들게 할때, 당신은 새로운 클래스를 수정하거나, 과거로 돌아갈 일이 없을꺼다.
  • 당신의 인터페이스에게 일반적인 기능을 제공하고, 에러를 방지하도록 디자인 해라. 부정확하게 사용하기 어렵게 하고, 정확하게 사용하기 쉽게 만들어라. 예를 들어서 클래스에 대한 복사나 할당에 대한 연산자를 없애서, 복사, 할당을 못하게 하라. 부분적인 할당에 대하여 옙아하라. (Item 33참고)
  • 만약, 당신의 코드를 구현 (generalize:일반화) 하기 위해서 큰 제한사항이 없다면, 구현(generalize:일반화) 해라. 예를들어서, 당신이 tree 검색 알고리즘을 작성하는 중이라면, 사이클이 없는 그레프에 대해 적용 시킬수 있는 일반화에 대한 궁리를 해라.

미래를 생각하는 것은 당신의 코드에 대한 재 사용성을 늘리고, 유지보수를 쉽게하며, 소프트웨어를 견고하게 만든다. 그리고 변화하는 환경에 우아하게 대처할 것이 확실하다. 미래에 대한 대처는 반드시 현재의 생각과 균형을 이루어야만 한다. 많은 프로그래머들이 현재 이외에는 생각을 하지 않는다. 하지만, 그래서 그들은 구현과 디자인에 긴 시각을 포기해야 한다. 다르게 하여라. 거부해라. 미래를 생각하는 프로그램을 만들어라.

2. Item 33: Make non-leaf classes abstract.

  • Item 33: 유도된 클래스가 없는 상태의 클래스로 추상화 하라.
당신이 동물의 역할을 하는 소프트웨어 프로젝트를 진행한다고 가정해라. 이 소프트웨어에서는 대부분의 동물들이 같게 취급될 수 있다. 그렇지만 두 종류의 동물들 -lizard(도마뱀) 와 chicken(닭)- 은 특별한 핸들링(handling)을 원한다. 그러한 경우에, 명백한 방법은 다음과 같이 관계를 만들어 버리는 것이다.


Animal 클래스는 주어진 모든 생명체들이 공유하고 있는 부분이다. 그리고 Lizard과 Chicken 클래스는 Animal에서 도마뱀과 닭만으로 특화된 클래스이다.

여기 이들 클래스들의 대강의 모습을 그린다.


~cpp 
 class Animal {
public:
	Animal& operator=(const Animal& rhs);
	...
};

class Lizard: public Animal {
public:
	Lizard& operator=(const Lizard& rhs);
	...
};

class Chicken: public Animal {
public:
	Chicken& operator=(const Chicken& rhs);
	...
};

보다시피 오직 할당(assignment) 연산자만 보인다. 그렇지만 이걸로 유지하는 것은 충분하다. 다음 코드를 생각해 보자.


~cpp 
    Lizard liz1;
    Lizard liz2;
    Animal *pAnimal1 = &liz1;
    Animal *pAnimal2 = &liz2;
    ...
    *pAnimal1 = *pAnimal2;
여기에는 두가지의 문제가 있다. 첫번째로 마지막 줄에 있는 할당 연산자는 Animal 클래스의 것을 부르는데, 객체 형이 Lizad형이라도 Animal 클래스의 연산자로 진행된다. 결과적으로, 오직 liz1의 Animal 부분만이 수정된다. 이것은 부분적인 할당(assignment)이다. liz1에 Animal 멤버의 할당은 li2로부터 얻은 값을 가진다. 그렇지만 liz1의 Lizard 부분의 데이터는 변화하지 않는다.

두번째 문제는 진짜 프로그래머들이 이와 같은 코드를 쓴다는 것이다. 특별히 C++로 전향한 C프로그래머들에 경험에서 보면, 포인터를 통한 객체의 할당은 그리 흔하지 않은것도 아니다. 그러한 경우는 이성적인 생각으로 취한 할당같이 보인다. Item 32의 촛점중, 상속 관계 상에서 우리의 클래스는 정확히 사용하기 쉽고, 부정확하게 사용하기 어렵게 해야 한다고 언급했다.

문제에 대한 한가지 접근으로 할당(assignment)연산자를 가상(virtual)로 선언하는 방법이 있다. 만약 Animal::operator= 가 가상(virtual)이면, 위의 경우에 할당 연산자는 정확한 Lizard 할당 연산자를 호출하려고 시도할 것이다. 그렇지만 만약 우리가 가상으로 할당 연산자를 선언했을때 다음을 봐라.


~cpp 
class Animal {
public:
    virtual Animal& operator=(const Animal& rhs);
    ...
};
class Lizard: public Animal {
public:
    virtual Lizard& operator=(const Animal& rhs);
    ...
};
class Chicken: public Animal {
public:
    virtual Chicken& operator= (const Animal& rhs);
    ...
};

언어상에 최근에 변화와 관계있는 의무로 우리는 할당 연산자에 대한 반환값의 최적화를 진행할수 있다. 그래서 각 반환 참조에 정확한 클래스로 교체 할수 있다 하지만 C++의 규칙은 모든 클래스 내부에, 가상 함수에 대하여 동일한 parameter 형을 규정 지을수 있다. 이것의 의미는 Lizard와 Chicken에 대한 할당 연산자가 반드시 할당시 right-hand 부분에 Animal의 어떠한(any) 한종류의 객체에 대한 준비를 해야만 한다는 것이다. 이는 우리에게 다음과 같은 코드가 합법임을 의미하는 것이다.


~cpp 
Lizard liz;
Chicken chick;

Animal *pAnimal1 = & liz;
Animal *pAnimal2 = & chick;
...
*pAnimal1 = *pAnimal2;  // chicken을 lizard에 할당한단 말이다.!

이것은 mix-type의 할당이다.:Lizard는 오른쪽의 Chicken의 왼쪽에 있는 입장이다. Mixed-type 할당은 C++에서 평범한 문제는 아니다. 왜냐하면 언어의 strong typing은 보통 그것이 규정에서 어긋나게 하기 때문이다. 하지만, animal의 할당 연산자를 가상으로 하는 것에 의해, 닫혀진 Mix-type 연산자의 문이 열려 버린다.

이것은 어려운 면이다. 우리는 same-type 할당만 포인터로 허용하게 만들어야 할것이다. 그에 반하여 같은 포인터를 통한 Mix-type 할당은 거부되도록 만들어야 한다. 다른 말로 하자면 우리는 이런건 통과지만


~cpp 
Animal *pAnimal1 = &liz1;
Animal *pAnimal2 = &liz2;
...
*pAnimal1 = *pAnimal2;      // 통과를 원해요! 같은 형이므로

그렇지만

~cpp 
Animal *pAnimal1 = &liz;
Animal *pAnimal2 = &chick;
...
*pAnimal1 = *pAnimal2;      // 안되길 원해요~

이러한 경우에 형을 가리는 것은 오직 실행 시간 중에 할수 있다. 왜냐하면 어떤때는, *pAnimal2를 *pAnimal1에 유효한 할당임을 알아 내야하고, 어떤때는 아닌걸 증명해야 하기 때문이다. 그래서 우리는 형 기반(type-based)의 실행 시간 에러의 거친 세계로 들어가게 된다. 특별하게, 만약 mixed-type 할당을 만나면, operator= 내부에 에러 하나를 발생하는 것이 필요하다. 그렇지만 만약 type이 같으면 우리는 일반적인 생각에 따라서 할당을 수행하기를 원한다.

우린 dynamic_cast(Item 2참고)를 이러한 행동에 적용해서 사용할수 있다. 여기 Lizard의 할당 연산자에 대한 방법이 있다.


~cpp 
Lizard& Lizard::operator=(const Animal& rhs)
{
    // lizard가 진짜 rhs가 맞는가?
    const Lizard& rhs_liz = dynamic_cast<const Lizard&>(rhs);

    *this에 rhs_liz의 일반적인 할당과정 수행을 한다.
}

이러한 함수는 *this가 오직 rhs가 Lizard일때만 할당을 허용한다. 만약 이것이 통과되지 못한다면, bad_cast 예외가 dynamic_cast에서 발생되어서 전달되어 진다. (정확히 표준 C++ 라이브러리에 있는 std::bad_cast 형의 객체가 날아간다. 이름 공간이 std에 관한것은 표준 라이브러리를 살피고 Item 35에도 설명되어 있다.)

예외 관한 주의가 필요 없는 보통의 경우에, 이 함수는 복잡하고, 비용 부담이 필요할 것으로 보인다.


~cpp 
Lizard liz1, liz2;
...
liz1 = liz2;    // dynamic_cast가 필요로 하지 않다. 이건 반드시 옳다.

우리는 Lizard에 좀더 적당한 할당 연산자를 더해서 dynamic_cast로 인해 발행하는 비용과 복잡성을 제가 할수 있다.


~cpp 
class Lizard: public Animal {
public:
    virtual Lizard& operator=(const Animal& rhs);
    Lizard& operator=(const Lizard& rhs);           // 더한 부분
    ...
};

Lizard liz1, liz2;
...
liz1 = liz2;                    // const Lizard&를 인자로 하는 operator= 호출

Animal *pAnimal1 = &liz1;
Animal *pAnimal2 = &liz2;
...
*pAnimal1 = *pAnimal2;      // const Ainmal&인자로 가지는 operator= 연산자 호출

const Animal&을 인자로 하는 연산자의 구현은 구현은 간단한다.


~cpp 
Lizard& Lizard::operator=(const Animal& rhs)
{
    return operator=(dynamic_cast<const Lizard&>(rhs));
}

이 함수는 rhs를 Lizard로 형변환 시킨다. 만약 형변환이 성공된다면 할당 연산자가 성공적으로 호출 될것이다. 반대라면 언급했던 bad_cast 예외가 발생된다.

솔직히, dynamic_cast를 사용해서 실행 시간에 검사하는건 좀 짜증난다. 한가지, 몇몇 컴파일러는 아직 dynamic_cast에 대한 지원이 부족해서, 이를 사용하면, 이론적으로는 이식성이 보장되지만 실제로는 그렇지 않다. 더 중요하게 Lizard와 Chicken의 클라이언트들은 bad_cast 예외에 대한 준비와, 할당을 수행할때의 각 코딩시에 민감하게 처리하지 못한다. 내 경험에 비추어 볼때 많은 프로그래머들이 그런 방법을 취하지 않고 있다. 만약 그들이 하지 않는다면, 할당이 일어나는 수많은 곳에서 정확하지 않은 처리상태로, 명료성을 보장 받을수 없다.

가상 할당 연산자를 이용하는 것 역시 불충분한 상태가 주어진다. 그것은 클라이언트가 문제있는 할당을 하는 것을 방지하는 방법을 찾도록 노력하는데 힘을쓰게 만든다. 만약 그러한 할당이 컴파일 중에 거부된다면, 우리는 잘못이 일어날 것에 대해 걱정할 필요가 없게된다.

가장 쉬운 방법은 Animal 내부의 operator=를 사역(private)로 묶어서 할당 자체를 하지 못하게 만들어 버리는 것이다. 그런 방법은 도마뱀이 도마벰에게, 닭이 닭에게만 할당을 할수 있지만, 부분적이고 Mix-type 할당을 방지한다.


~cpp 
class Animal {
private:
    Animal& operator=(const Animal& rhs);       // 사역(private)이다.
    ...
};

class Lizard: public Animal {
public:
    Lizard& operator=(const Lizard& rhs);
    ...
};

class Chicken: public Animal {
public:
    Chicken& operator=(const Chicken& rhs);
    ...
};

Lizard liz1, liz2;
...
liz1 = liz2;                    // 옳바르다.

Chicken chick1, chick2;
...
chick1 = chick2;                // 역시 옳바르다.

Animal *pAnimal1 = &liz1;
Animal *pAnimal2 = &chick1;
...
*pAnimal1 = *pAnimal2;          // 에러! Animal::operator=가 사역이라 에러난다.
불행히 Animal은 concrete 클래스이다.(가상이 아니란 소리) 그리고 이러한 접근은 Animal 객체 간에 할당을 못하는 것을 의미한다.

~cpp 
Animal animal1, animal2;
...
animal1 = animal2;      // 에러! Animal::operator=가 사역(private)이다.
게다가 Lizard와 Chicken에 구현된 할당 연산자도 정확히 구현되기 불가능하다. 왜냐하면, 유도된 클래스에서 할당 연산자는 그들의 기초 클래스의 할당 연산자의 호출을 책임진다.

~cpp 
Lizard& Lizard::operator=(const Lizard& rhs)
{
    if (this == &rhs) return *this;

    Animal::operator=(rhs);     // 에러! 사역(private) 인자의 함수를 호출하는 시도를 한다.
                                // 하지만 Lizard::operator=는 반드시 이 함수를 Animal 부분
                                // 의 할당을 위해 호출해야 한다.
    ...
}    
이러한 문제를 Animal::operator=를 보호(protected)영역으로 설정해서 해결할수 있다. 하지만 Animal 포인터를 통하여 Lizard와 Chicken객체의 부분적인 할당을 막는 것에 비하여, Animal 객체 간의 할당 문제는 난제이다. 추상 클래스로서 Animal 은 초기화 할수 없다. 그래서 Animal 간의 할당은 허용될 필요가 없다. 물론 새로운 문제를 수반한다. 왜냐하면 우리의 기본적인 디자인에서 이 시스템에서는 Animal을 객체로서 필요가 있어서 지원한 것 이기 때문이다. 이러한 쉬운 방법은 어려운 부분이 둘러싸고 있는 형국이다. 대신에 Animal 을 추상화 시키는 클래스를 새로 만들어 버리는 건 어떨까? AbstractAnimal 같이 말이다. 대신에 이제 이들을 Animal, Lizard, Chicken 객체가 상속을 받고 객체로서 활용 되는 것이다. 그렇게 되면 우리는 AbstractAnimal 이라는 추상 클래스에서 Concrete 클래스를 유도한다. 이러한 계층도를 준비하면 다음과 같다.


그리고 다음과 같이 정의 된다.

~cpp 
class AbstractAnimal {
protected:
    AbstractAnimal& operator=(const AbstractAnimal& rhs);
public:
    virtual ~AbstractAnimal() = 0;        // 이에 관해서는 설명에 자세히
    ...
};

class Animal: public AbstractAnimal {
public:
    Animal& operator=(const Animal& rhs);
    ...
};

class Lizard: public AbstractAnimal {
public:
    Lizard& operator=(const Lizard& rhs);
    ...
};

class Chicken: public AbstractAnimal {
public:
    Chicken& operator=(const Chicken& rhs);
    ...
};

이 디자인은 당신이 필요한 모든것을 제공한다. 동종의 할당에 관해서 동물, 도마뱀, 의 할당 연산을 허용한다.;부분적인 할당과 타종간의 할당을 금지한다는 것;그리고 유도된 클래스의 할당은 아마 기본 클레스 내의 할당 연산자가 맡을 것이다. 게다가 Animal, Lizard, Chicken클래스의 역할이 기록된 코드들을 수정을 필요로 하지 않는다. 왜냐하면, 이들 클래스는 소계된 AbstractAnimal 클래스로 기존의 역할들을 대신 받을수 있다. 물론, 그러한 코드들은 재 컴파일이 되어야 한다. 그렇지만 컴파일러가 할당의 명시성을 보장해 주어서 보장되는 보안성에 비하여 작은 비용이다.

모든 일에 대하여 AbstractTnimal은 반드시 추상적이어야 하나? 그것은 반드시 최소 하나의 순수 가상 함수를 가지고 있어야 한다. 대부분의 경우에 알맞는 함수를 고르기에는 별 문제가 없다. 그렇지만 희귀한 경우에 당신은 아마 AbstractAnimal 같은 클래스를 만들어야 하는 상황에 직면할지도 모른다. 그러한 경우에, 적당한 방법은 순수 가상 함수로 파괴자를 만들어 버리는 것이다.;위에서 보는것과 같이 말이다.포인터를 통한 다형성을 지원하기 위하여 기본 클래스는 가상 파괴자를 어떤 방법으로든 필요로 한다. 그래서 순수 가상함수를 만들기 위한 비용만이 해당 클래스 정의부 바깥에서 이루어질 구현에서 지불된다. (에를들어서 p195를 보아라)

(순수 가상 함수에 대한 구현의 개념이 당신을 혼란스럽게 한다면, 그거에 신경을 끊으면 된다. 순수 가상 함수의 선언은 구현이 없는것을 의미 하지만은 않다. 그것은 의미한다.
  • 현재의 클래스를 추상화 한다. 그리고
  • 현재 클래스에서 유도된 어떠한 concrete 클래스라도 반드시 "보통" 가상 함수로 함수를 선언해야 한다. (다시 말해 "=0"을 제외하고)
맞다, 대부분의 순수 가상 함수는 결코 구현되지 않는다. 그렇지만 순수 가상 파괴자는 특별한 경우이다. 그들은 반드시 구현되어야 한다. 왜냐하면 그들은 유도된 클래스들이 파괴될때 마다 불리기 때문이다. 게다가 그들은 종종 유용한 작동을 하는데, 자원의 해제 같은거(Item 9참고)나 로그 메세지를 남기는것 따위 말이다. 순수 가상 함수의 구현은 일반적으로 아마 특별하지 않은 경우이다. 하지만 순수 가상 파괴자는 그렇지 하다 그것은 명령에 가깝다.)

당신은 아마도 데이터 멤버를 가지는 Animal 클래스 같이, Concrete 기초 클래스를 기반으로 전체하고 기초 클래스의 포인터를 통해서 할당에 대한 논의라는걸 주목할 것이다. 그렇다면, 만약 아무런 데이터가 없다면, 의도에 부합하는, 문제가 안될것이 없고, 좀더 생각해 보면, 그것은 자료가 없는 concrete 클래스가 두번 상속 되어도 안전할꺼라고 의견을 펼지 모른다.

두가지 경우에 한가지는 당신의 데이터가 없는 concrete로 적용한다.:이건 미래에 데이터를 가질지도, 안가질지도 모른다. 만약 미래에 데이터를 가진다면, 당신이 하는 모든 것은 데이터 멤버가 추가도리때까지 문제를 미루어 두는 것이다. 이런 경우 당신은 잠깐의 편함과 오랜 시간의 고뇌를 맞바꾸는 것이다. (Item 32참고) 대안으로, 만약 기초 클래스가 정말 어떠한 데이터도 가지고 있지 않다면, 처음에 추상화 클래스와 아주 비슷한 이야기가 된다. concrete 기본 클래스는 데이터 없이 사용되는건 무엇인가?

AbstractAnimal 같은 추상 기본 클래스를 Animal 같은 concrete 기본 클래스로의 교체는 operator= 의 동작을 도 쉽게 이해할수 있는 장점을 가져다 준다. 또한 당신이 배열을 다형하게 다루는 기회 역시 줄여준다.(배열과 클래스에 대한 관계는 Item 3을 참고하라.) 그렇지만, 기술적으로 가장 두드러지는 이점은 디자인 레벨에서 나타난다. 왜냐하면 당신은 유용한 추상의 존제를 명시적으로 인지 할수 있기 때문이다. 그것은 유용한 개념의 존재를 의식하지 않을지라도, 당신으로 하여금 새로운 유용한 개념을 위한 추상 클래스 생성하게 한다.

만약 당신이 C1,C2 두개의 concrete 클래스를 가지고 있고, C2는 C1을 public 상속 했을때 당신은 아마도 두 클래스의 관계를 A라는 새로운 클래스를 생성해서, 다시 구조를 재편하는 과정을 부담없이 할수 있다.


이러한 변환 과정에서 처음의 값은 추상 추상 클래스 A 를 확인하게 만든다. C1과 C2는 아마 보통 몇가지를 가지고 있다.:그것은 그들이 public 상속이 되는 이유이다. 이 변환으로 당신은 반드시 그 가지고있는 어떻것을 확인해야 한다. 게다가 C++에서 클래스로 모호한 부분에 대하여 명확하게 해주어야 한다. 그것은 보통 추상화(abstraction)가 추구해야 하는 것이고 잘 정의된 멤버 함수와 확실한 문법으로 구현된다.

이 모든것이 어떤 잘못된 생각으로 인도한다. 결국, 모든 클래스는 어떠한 종류의 추상화를 표현한다. 그래서 우리는 계층 관계에서 모든 개념을 위해서 두가지의 클래스를 생성할수가 없게 되지 않을가? 하나는 추상화로(추상화를 표현하는 부분 작성) 하나는 concrete로(객체 생성 부분 작성)? 아니다. 만약 당신이 그렇게 하면 당신은 굉장히 많은 클래스로 계층 관계를 만들 것이다. 그리고 컴파일에도 많은 비용을 소요한다. 그것은 객체 지향 디자인의 잘못된 목표이다.

그 목표는 유용한 추상화와 추상화를 추상 클래스로 존재하게 강제해 버리 도록 구분한다. 그렇지만 당신은 어떻게 유용한 추상화를 분간 할것인가? 누가 그 추상화가 미래에도 유용한지 알려주는가? 누가 어디로 부터 상속되는지 예상할수 있는가?

자, 나는 어떻게 미래에 상속 관계에 대한 사용을 예측할수 없다. 그러나 나는 이거 하나는 알고 있다.:하나의 목적에서 추상화에 대한 필요는 비슷할 것이라는것, 그러나 더 많은 목적으로 추상화에 대한 필요성은 보통 중요하다. 즉, 유용한 추상화는 하나 이상의 용도(목적)에서 필요성이 있다. 그것은 그렇나 추상화가 그들에 목적에 부합하는 클래스와 올바르게 쓰인다는 것과, 유도된 클래스에서도 또한 유용하다는 걸 의미한다. (작성자주:의역)

이것은 정확하게 concrete 기본 클래스가 유용한 추상 기본 클래스로 변환되는 이유가 된다.:그것은 오직 존재하는 concrete 클래스가 기본 클래스 같을때, 다시 말하면 클래스가 새로운 목적으로 재사용 될때, 새로운 추상 클래스의 도입을 강요한다. 그러한 추상화는 유용하다. 왜냐하면 그들 자신이 보이는 것과 같이 구현될 필요성을 가지고 있기 때문이다.

처음에 요구되는 개념은, 우리는 추상 클래스(개념을 위한)와 concrete 클래스(객체가 개념에 반응하기 위한) 양쪽다 정당화 시킬수 없다. 하지만 두번째로 필요한 개념은, 우리가 이 두 클래스의 생성을 정당화 할수 있다. 내가 간단하게 언급한 변환은 이러한 과정(process)를 공정화(mechanize) 하는 것이다. 그리고 비록 디자이너와 프로그래머들이 유용한 개념을 항상 의식을 가지고 생각하지 않을지라도, 그들에게 생각하는 유용한 추상화 과정을 명시적으로 보이도록 강제한다.

믿음직한 예제를 생각해 보자. 당신이 네트웍 상에서 어떤 프로토콜을 이용해서 정보를 패킷 단위로 나누어 컴퓨터 사이에 이동시키는 어플리케이션을 작성한다고 하자.(모호 그래서 생략:by breaking it into packets) packet을 표현하는 클래스나 클래스들에 관하여 생각해야 할것이다. 그러한 클래스들은 이 어플리케이션을 대하여 잘알고 있어야 한다고 전제 할것이다.

일단, 오직 한종류의 프로토콜을 통하여 오직 한종류의 패킷만을 전송한다고 가정하자. 아마도 다른 프로토콜과 패킷의 존재를 알고 있을 것이다. 그렇지만 당신은 그들을 지원하지 않고, 미래에도 이들을 지원할 계획이 없을 것이다. 당신은 패킷에 대한 추상 클래스(패킷을 표현하는 개념을 위한)를 사용할 패킷의 concrete 클래스와 같이 만들것인가? 만약 그렇게 한다면 당신이 패킷의 기본 클래스의 변화 없이 차후에 새로운 패킷 형태를 추가하는 것을 바랄수 있다. 그것은 새로운 형태의 패킷이 추가될경우 패킷과 관련하여 재컴파일할 수고를 덜어 줄것이다. 그렇지만 그런 디자인은 두개의 클래스를 요구한다. 그리고 지금 당신에게는 하나만이 필요하다.(당신이 사용하는 패킷만을 표현하는 클래스) 없을지도 모르는 미래를 위한 확장을 위해서 당신의 디자인을 복잡하게 하는 것을 해야 할까?

여기에는 정확한 답을 내릴수 없다. 그렇지만 경험상으로 그것은 우리가 완전히 이해하기 힘든 개념을 잘 구현한 훌륭한 클래스의 디자인에는 결코 가까워 질수 없을 것으로 보인다. 만약 당신이 패킷을 위해서 추상 클래스를 만들었다면, 오직 단일 패킷 형태로 제한하는 디자인 이후에 어떻게 옳바른 디자인을 할수 있겠는가? 기억해 봐라, 만약 당신이 추상 클래스로 디자인해서 미래에 이를 상속한 클래스들로 디자인상 별 변화 없이 제작될수 있다는 면, 이런 추상 클래스가 주는 장점을 얻는다. (만약 변화가 필요하다면 모든 클라이언트에게 재 컴파일을 요구해야 한다. 그리고 아무것도 얻지 못한다.)

당신이 하려는 훌륭한 추상 패킷 클래스 디자인은 당신이 다양한 목적에 수많은 다른 패킷을 훌륭하게 만들어 보지 않고서는 할수 없다. 이번 경우에서 이런 제한된 경험을 제시하는 것은 나의 충고가 패킷에 대한 정의가 아니라, 추후 오직 concrete 패킷 클래스로 부터 상속의 필요성이 있을때에, 패킷의 추가를 용이하게 하기 위한 것이다.

내가 여기에 제시한 변환은 추상 클래스의 필요성을 확인하기 위한 하나의 방법이지 유일한 방법은 아니다. 추상 클래스의 지원이 요구되는 수많은 경우들이 있다.;객체 지향에 분석은 책들을 만들 만큼 다양하다. 추상 클래스에 관한 소개는 이 경우 만이 아니라 자신 스스로 다른 concrete 클래스에 대한 상속 관계를 설계하면서 깨달아라. 그렇지만, 보통 두개의 concrete 클래스 public 상속으로 연결 지어 놓는것은 새로운 추상 클래스의 필요성을 의미한다.

종종 다음의 문제로, 심사 숙고해서 만들어논 평화로운 이론을 가혹한 현실이 망친다. 서드 파티 C++ 라이브러리는 폭팔적으로 증가하고, 당신은 읽을수 밖에 없는 라이브러리 상의 concrete클래스로 부터 상속받은 concrete 클래스의 생성을 원할때 어떻게 할것인가?

당신은 새로운 추상 클래스를 넣기위해, 라이브러리를 수정할수 없다. 그래서 당신의 선택은 제한과 별로 매력적이지 않는 방법일 것이다.

  • 당신의 concrete 클래스를 존재하고 있는 concrete 클래스로 부터 유도하라 그리고 할당에 관련한(assignment-related) 문제들인, 이 Item의 시작 부분에 대하여 시험해 봐라. 또한 Item 3에 언급된 배열에 관한 문제가 있는지도 점검해 봐야만 한다.
  • 당신이 필요로 하는 것에 가장 가까운 추상 클래스를 상속 계층 높은 부분에서 찾아봐라, 그리고 나서 그 클래스에서 상속하라. 물론 정확하지 않은 클래스 일지도 모른다. 그렇더라도, 아마 당신은 확장하고자 하는 기능을 가지는 concrete 클래스의 구현의 노력 해야 할것이다.
  • 당신의 새로운 클래스를 당신이 상속 받고자 하는것과 비슷한 클래스 라이브리의 한부분에 구현해라. 예를 들어서 당신이 데이터 멤버로서 라이브러리 클래스의 객체를 가지고 싶을때, 당신의 새로운 클래스에 라이브러리 클래스의 인터페이스를 재정의 해라.
    ~cpp 
    class Window {              // 이것은 클래스 라이브러리
    public:
        virtual void resize(int newWidth, int newHeight);
        virtual void repaint() const;
    
        int width() const;
        int height() const;
    };
    
    class SpecialWindow {       // 이것은 Window로 부터 상속되기 원하는 클래스
    public:
        ...
    
        // 비 가상 함수는 넘기는 것으로 구현한다.
        int width() const { return w.width(); }
        int height() const { return w.height(); }
    
        // "상속 받은" 가상 함수의 새로운 구현
        virtual void resize(int newWidth, int newHeight);
        virtual void repaint() const;
    
    private:
        Window w;
    };
    
    이러한 전략은 당신이 의존하고 있는 라이브러리 벤더의 클래스가 업데이트 될때 마다 당신의 클래스를 업데이트를 할 준비가 되어 있는걸 요구한다. 또한 라이브러리 클래스상에서 가상 함수의 재정의 능력을 제거를 요구하기도 한다. 왜냐하면 당신은 상속 받기 전까지 가상 함수를 재정의 할수가 없다.

  • 당신이 가진 것을 이용해 만들어라. 라이브러리에서 concrete 클래스의 사용하고, 당신의 소프트웨어에서 수정하라 그러면 , 그 클래스는 충분하다. 당신이 클래스에 더하고자 하는 기능을 제공하는 non-member 함수를 작성하라. 하지만 할수 없다. 그와 같이 하면 소프트웨어의 결과는 명료하지 못하고, 효율적이지 못하고, 유지 보스하기 힘들고, 확장하기 힘들다. 하지만 최소한 그런 일을 할수 있게 하라.

이러한 선택은 특별히 매력적이지는 않다 왜냐하면 당신은 어떤 공학적 판단에 적용해야 하고, 최대한 모호하지 않도록 하는 독을 선택해야 한다. 그리 재미있는건 아니다. 그러나 삶은 때로 그와 같다. 미래의 당신 자신을 위해서 더 쉽게 만들어라, 당신이 원하는 디자인의 라이브러리의 벤더들에게 불평해라.


아직 일반적인 규칙이 남아 있다.:non-leaf 클래스가 추상화 되어야 한다. 당신은 아마도 외부의 라이브러리를 사용할때, 묶어 줄만한 규칙이 필요할 것이다. 하지만 우리가 다루어야 하는 코드에서, 외부 라이브러리와 가까워 진다는 것은 신뢰성, 내구성, 이해력, 확장성에서 것과 떨어지는 것을 야기한다.

3. Item 34: Understand how to combine C++ and C in the same program

  • Item 34: 같은 프로그램에서 C++와 C를 혼합하는 법 이해해라.

많은 면에서, C++와 C에서 컴포넌트를 만들때, 네가 하는 걱정은 C 컴파일러가 오브젝트 파일을 서투르게 처리 할때의 걱정과 같다. 다른 컴파일러들이 구현에 의존적인 요소들에 대하여 동일하지 않으면, 그런 파일들을 혼합해서 쓸 방법이 없다. (구현 의존 요소:int, double의 크기, 인자를 넘기고 받는 방법, 호출자와 호출간에 통신 ) 이러한 개발 환경에서 컴파일러들을 섞어서 사용하는 것에(mixed-compiler) 관한 실질적은 관점은 언어의 표준에 대한 노력에 의해서 아마 완전히 무시 된다. 그래서 컴파일러 A와 컴파일러 B의 오브젝트 파일을 안전하게 섞어서 쓸수 있는 신뢰성 있는 유일한 방법은, 컴파일러 A,B의 벤더들이 그들의 알맞는 output에 대한 product의 정보를 확실히 아는 것이다. 이것은 C++와 C를 이용하는 프로그램, 그런 모든 프로그램에 대하여 사실이다. 그래서 당신이 C++과 C를 같은 프로그램에서 섞어서 쓰기 전에는 C++와 C컴파일러가 알맞는 오브젝트 파일을 만들어 내야만 한다.

여기에서 우리가 생각해볼 관점은 총 네가지가 필요하다.:name mangling, initialization of statics, dynamic memory allocation, and data structure compatibility.

3.1. Name mangling : 이름 조정

당신도 알다 시피, name mangling(이름 조정:이후 name mangling로 씀) 당신의 C++ 컴파일러가 당신의 프로그램상에서 각 함수에 유일한 이름을 부여하는 작업이다. C에서 이러한 작업은 필요가 없었다. 왜냐하면, 당신은 함수 이름을 오버로드(overload)할수가 없었기 때문이다. 그렇지만 C++ 프로그래머들은 최소한 몇개 함수에 같은 이름을 쓴다.(예를들어서, iostream 라이브러리를 생각해보자. 여기에는 몇가지 버전이나 operator<< 와 operator>>가 있다. ) 오버로딩(overloading)은 대부분의 링커들에게 불편한 존재이다. 왜냐하면 링커들은 일반적으로 같은 이름으로 된 다양한 함수들에 대하여 어두운 시각을 가진다. name magling은 링커들의 진실성의 승인이다.;특별히 링커들이 보통 모든 함수 이름에 대하여 유일하다는 사실에 대하여

C++의 테투리에 머물러 있다면, name mangling은 당신과 그리 관계 있는 것 같지 않다. 만약 당신이 컴파일러가 xyzzy라고 magling한 drawLine이라는 함수를 가지고 있다면 당신은 항상 drawLine이라고 사용하고, 오브젝트 파일에서 xyzzy라고 교체되어 쓰는것에는 주의를 기울일 필요가 없다.

하지만 C 라이브러리에서 drawLine이라면 이야기가 달라진다. 그런 경우에 당신의 C++소스 파일은 아마도 이것과 같이 선언된 헤더 파일을 가져야 할것이다.

~cpp 
    void drawLine(int x1, int y1, int x2, int y2);
그리고 당신의 코드는 일반적으로 사용하는 것처럼 drawLine을 호출하는 코들르 가진다. 그러한 각 호출마다 당신의 컴파일러가 mangle을 한 함수 이름을 호출 부분에 넣는다. 그래서 이론 코드를 쓰면,

~cpp 
drawLine(a, b, c, b);  // mangle 처리가 안된 상태
당신의 오브젝트 파일은 다음과 같은 모습의 함수 호출을 가진다.

~cpp 
xyzzy(a, b, c, d);  // mangle한 후의 함수 이름
그렇지만 만약 drawLine가 C함수라면, drawLine 함수를 호출할때 drawLine으로 포함하는 컴파일러된 버전으로 오브젝트( 혹은 동적 링크 라이브러리 등) 파일에 포함되어 있다.;name mangle이 되지 않은 체로 되어 있다. 당신이 이 둘을 모두 섞어서 프로그램 하려고 노력하면, 에러가 날것이다. 왜냐하면 링커는 xyzzy의 호출되는 함수를 찾고, 그에 관한 함수가 없기 때문이다.

이런 문제를 해결하기 위하여, C++ 컴파일러에게 해당 함수에 name mangle을 수행하지 않도록 알려야 할 방법이 필요하다. C든, assempler, FORTRAN, Lisp, Forth나 니가 가진 무슨 언어간에, 다른 언어에서 작성되어진 name mangle 처리된 함수를 원할수 없다.(예, 이 언어들에 COBOL도 들어가겠지만 당신이 쓰는가?) 결곡, 만약 C함수인 drawLine을 호출하면 그것은 진짜로 drawLine을 호출하고, 당신의 오브젝트 코드역시 그 이름 그대로 변화없이 사용한다.

이러한 name mangle을 막기 위하여, C++의 에서는 extern "C"를 직접 사용한다.

~cpp 
// name mangle하지 않는 처리
extern "C"
void drawLine(int x1, int y1, int x2, int y2);
extern "C" extern "Pascal", extern "FORTRAN"

extern "C"를 쓰는것을 당연시 생각하는 함정에 빠지지 마라, extern "Pascal" 이나 extern "FROTRAN" 만이 쓰이는 경우도 있다.하지만 최소한 표준은 아니다. 가장 좋은 방법인 extern "C"는 C에서 작성된 함수들과 관계 있다는 것을 확신하는건 아니지만, 만약 C에서 작성된 것처럼 해당 함수를 호출할 수 있다는 의미이다. (기술적으로 extern "C" 는 C와의 연결을 가진 함수를 의미하지만, 꼭 그런것 만은 아니다. 그렇지만 name mangle을 방지 한다는 것 만은 확실하다.)

예를들어서 만약 당신은 너무나 불행하게도 어셈블러에서 작성한 함수와 같은 경우라면 당신은 역시 extern "C" 를 선언할수 있다.:

~cpp 
// 이 함수는 어셈블러에 있다. 함수를 name mangle하지 않는다.
extern "C" void twiddleBit(unsigned char bits);

당신은 C++의 함수를 선언할때조차 extern "C"를 사용할수 있다. 이것은 C++에서 라이브러리를 작성할때 다른 프로그래밍 언어들에 대한 지원을 하고 싶을때 유용하다. C++함수에 대하여 name mangle을 금지해서 당신의 클라이언트들이 당신이 mangle을 적용한 함수 대신에, 자연스럽게 명시적으로 사용할수 있다.

~cpp 
// C++ 을 따르는 함수는 C++의 외부에서 쓰이도록 설계되었다.
extern "C" void simulate (int iterations);
종종 당신은 name mangle를 원하지 않은 함수를 많이 가지고 있을 것인데, 이 모든것 앞에 모두 extern "C" 를 붙이기에는 귀찮다. 다행스럽게도 이에 대한 지원 역시 다음과 같은 모습으로 표현할수 있다.

~cpp 
extern "C" {    // 이 영역의 함수는 모두 name mangle가 적용되지 않는다.
    void drawLine(int x1, int y1, int x2, int y2);
    void twiddleBits(unsigned char bits);
    void simulate(int iterations);
  ...
}
extern "C"의 이러한 사용은 C++과 C를 사용해야만 하는 헤더파일의 유지보수를 간편하게 해준다. C++로 컴파일 할때는 extern "C"를 허용하고, C로 할 때는 허용하지 않고 컴파일 하면 된다. 이러한 preprocessor에 대한 심벌(symbol)은 __cplusplus 가 C++ 컴파일 타임에 정의되어 있는 것을 이용해서 다음과 같이 구성할수 있다.

~cpp 
#ifdef __cplusplus
extern "C" {
#endif
    void drawLine(int x1, int y1, int x2, int y2);
    void twiddleBits(unsigned char bits);
    void simulate(int iterations);
  ...
#ifdef __cplusplus
}
#endif

그런 방법에 이용하는건, "표준" 적인 name mangle 알고리즘이란 없다. 다른 컴파일러는 다른 방법으로 name mangle 을 막는 방법을 제공한다. 이는 좋은 것이다. 만약에 모든 컴파일러가 같은 방법으로 name mangle을 수행 하면, 당신은 아마도 그들이 만들어 내는 알맞은 코드에 대한 생각에 안심해 할지 모른다. 만약 당신이 정확하지 않은 C++ 컴파일러로 부터 생성된 객체를 혼용하면 링크중에 에러를 발생할수 있는 좋은 기회를 맞이할것이다. 왜냐하면, mangle처리된 이름을 찾을수 없기 때문이다. 이것은 당신에게 알맞음을 따지는 또다른 문제를 의미하고, 또 도좋은 해결책을 찾아야 함을 의미한다.


3.2. Initialization of statics : static 인자의 초기화


일단 name mangle에 관한 내용을 익혔다면, C++코드에서 main 이전과 이후에 많은 코드들이 수행된다는 사실에 관해서 생각해볼 필요가 있다. 특별히, 전역 이름 공간, 파일 영역상의 정적(static) 클래스 객체는 보통 main 보다 먼저 실행된다. 이 과정이 static initialization 이라고 알려져 있다. 이는 프로그램의 실행시점 간에 C와 C++ 프로그램에 대한 방법으로 완전히 다른 방향을 취한다. 비슷하게, static initialization으로 만들어진 객체는 반드시 그들의 파괴자를 static destruct 동안에 불러주어야 한다.;그러한 과정은 일반적으로 main 이후에 진행된다.

main이 수행되기 전에 객체가 생성되야 하는 필요성이 있지만, main이 가장 처음 불려야 하는것으로 가정되는 딜레마의 해결을 위해서, 많은 컴파일러 들이 특별한 컴파일러가 작성하는 함수를 main의 시작단에 넣는다. 그리고 이 특별한 한수는 static initialization 을 한다. 비슷하게 컴파얼러는 종종 main의 끝에 특별한 함수를 넣어서 static 객체의 삭제를 수행한다. main 에 대해 작성되는 코드는 종종 main 이 엃게 작성되는 것처럼 보인다.


~cpp 
int main(int argc, char *argv[])
{
    performStaticInitialization();         // main이 넣는다고 가정하는 함수

    여기는 메인의 코드들을 수행;

    performStaticDestruction();            // main이 넣는다고 가정하는 함수
}

이렇게 의미 그대로는 아니다. performStaticInitialization 과 performStaticDestruction 은 보통 훨씬 난해한 이름이고, 그들은 inline 으로 우리의 객체 파일에서 어떠한 함수로 알아볼수 있게 작성되어 있다. 중요한 점은 이것이다.:만약 C++ 컴파일러가 정적(static) 객체의 초기화(initialization)와 파괴(destruction)를 이러한 방법으로 적용하면, C++에서 main이 작성되지 않았다면, 초기화와 파괴가 수행되지 않을 것이다. 정적 초기화, 파괴에 대한 이러한 접근이 일반적이기 때문에, 만약 C++에서 소프트웨어 시스템의 어떤 부분을 작성할때 main을 작성하도록 노력해야 할것이다.

때때로 C에 main 작성이 더 가치 있다고 보인다. - 대다수 프로그램이 C이고, C++이 단지 라이브러리 지원 이라면 이라고 말해라. 그렇기는 하지만, C++ 라이브러리는 정적(static) 객체(object)를 포함하는 것이 좋다.(좋은 기능이 많다는 의미) (만약 지금 없다해도 미래에 어쩌면 있을지 모르지 않는가? Item 32참고) 그래서 가능하다면 C++에서 main을 작성은 좋은 생각이다. 그것은 당신의 C코드를 제작성 하는것을 의미하지는 않는다. 단지 C에서 쓴 main을 realMain으로 이름만 바꾸고, main의 C++버전에서는 realMain을 호출한다.:

~cpp 
extern "C"                                
int realMain(int argc, char *argv[]);     // C 에서 구현된 함수

int main(int argc, char *argv[])          // C++에서 이렇게 작성
{
    return realMain(argc, argv);
}
이렇게 한다면, main위에 주석을 주는것이 좋은 생각일 것이다.

만약 C++에서 main을 작성할수 없다면 문제가 된다. 왜냐하면, 정적(static) 객체 호출을 위한 생성자, 파괴자에 대하여 이식성에 확신을 줄수 없기 때문이다. 이것은 모든것을 잃는다는 의미는 아니다. 단지 좀더 할일이 많아 진다는 것을 의미한다. 컴파일러 밴더들은 이러한 문제를 잘 알고 있다. 그래서 거의 대부분의 벤더들은 static initialization, destruction을 위해서 몇가지의 언어와 관계없는 기술을 제공한다. 이에 관한 정보는 당신의 컴파일러의 문서를 참조하거나, 벤더들에게 문의해라

3.3. Dynamic memory allocation : 동적 메모리 할당


동적 메모리 할당(dynamic memory allocation:이하 동적 메모리 할당)에 관한 문제가 우리에게 주어진다. 일반적인 규칙은 간단하다.:C++ 에서 new, delete 부분 (Item 8참고) 그리고 C 프로그래밍 에서는 malloc(그리고 그것의 변수들) 과 free이다. malloc으로 얻은건 free로, new로 얻은건 delete로 해재하는한 모든 것이 올바르다. 그렇지만, new로 할당된 메모리 영역을 가리키는 포인터를 free로 해제 시키는 호출은 예측 못하는 수행을 가지고 온다. 마찬가지로 malloc로 얻은 것을 delete로 해제하는 것도 예측할수 없다. 그렇다면, 이를 기억하기 위한 방법은 new와 delete, malloc와 free를 엄격하게 구분해야 하는 것이다.


때때로, 이것은 말하기에 쉬워 보인다. C, C++ 양쪽다 표준은 아니지만 그럼에도 광범위하게 쓰이는, 저급의 strdup 함수에 관하여 생각해 보자.

~cpp 
char * strdup(const char *ps);  // 문자열의 사본을 반환한다.
메모리 leak를 피할려면, strdup 내부에서 할당된 메모리를 strdup의 호출자가 해제해 주어야 한다. 하지만 메모리를 어떻게 해제 할것인가? delete로? free로? 만약 strdup를 당신이 C 라이브러리에서 불렀다면, free로, C++ 라이브러리에서 불렀다면 delete로 해야 한다. 무엇이 당신은 strdup 호출후에 무엇이 필요한가, 시스템과 시스템 간에 뿐이 아닐, 컴파일러, 컴파얼러 간에도 이건 문제를 일으킬수 있다. 그러한 이식성의 고통의 감소를 위해, 표준 라이브리에서 불러야 하지 말아야할 함수, 대다수 플렛폼에 종속적으로 귀속된 모습의 함수들을 부르지 않도록 노력하라.

3.4. Data Structure Compatibility : 자료 구조의 호환성(이식성)


우리는 C++와 C프로그램 사이에 데이터 교환에 관해서 다룬다. C++의 개념을 이해하는 C 함수를 만드는것 불가능 하다. 그래서 이 두 언어간의 전달의 수준은 C가 표현할수 있는 개념으로 한정된다. 그래서 객체의 전달 방식이나, 포인터를 C 에서 작성된 루틴의 멤버 함수로 전달하는 방법은 이식성이 떨어질것은 분명하다. C는 일반적인 포인터 이해한다. 그래서 당신의 C++와 C컴파일러가 만들어 내는, 두가지의 언어에서 알맞는 함수는 pointer와 객체를 pointer와 non-member 나 static 함수를 안전하게 교체할수 있다.자연 스럽게, 구조체와 built-in형의 변수들 역시 자유로이 C+++/C의 경계를 넘나든다.

C++에서 구조체의 설계 규칙이 C에서의 그것과 일치하기 때문에 양 언어가 각자의 컴파일러로 같은 규칙으로 구조체가 설계되어 있다고 가정 할수 있다. 그러한 구조체는 안전하게 C++과 C사이에 교환될수 있다. 만약 당신이 비가상 함수를 C++ 버전의 구조체에 추가 했다면, 그것의 메모리 모양(layout)은 바뀌지 않는다. 그래서 비가상 함수를 포함하는 구조체(혹은 클래스)의 객체(object)는 오직 멤버 함수가 없는 구조체 C로 최적화 될것이다. 가상 함수를 더하는 것은 논할 가치가 없다. 왜냐하면 가상 함수를 클래스에 추가하는 것은 메모리의 배열에 다른 모습을 보인다. (Item24참고) 다른 구조체(혹은 클래스)로 부터 상속 받은 구조체는 보통 그것의 메모리상 모습이 바뀐다. 그래서 기본(base) 구조체(혹은 클래스)에 의한 구조체 역시 C함수의 지원이 미흡하다.

자료 구조의 입장은 이렇게 요약된다.:구조체의 정의를 C++와 C에서 전부 컴파일되도록 만들면, C++에서 C로의 구조체의 전달과, C에서 C++로의 전달은 안전하다. 비가상 멤버 함수를 C++ 버전의 구조체에 더하는 것은, 다른 한편으로 C에 맞추는 것이기에 이것도 두 언어간에 조화에 영향을 끼치지 안흔ㄴ다. 하지만 구조체가 몇가지의 변화이 안될 것이다.


3.5. Summary : 요약

만약 당신이 C++와 C를 같은 프로그램에서 섞어서 쓰기를 원한다면, 간단한 가이드 라인을을 따를것을 기억하라.

  • C++, C컴파일러가 조화로운 오브젝트 파일을 생성하도록 하라.
  • 두언어에서 모두 extern "C" 를 사용해서 함수를 선언하라
  • new로 할당 받은 메모리는 항상 delete로 제거하라;malloc로 할당한 메모리는 항상 free로 제거하라.
  • 두 언어상에서 C가 컴파일할수 있는 데이터 구조의 전달로 제한 된다.:C++버전의 구조체는 아마도 비가상 멤버 함수만을 가지고 있어야 한다.

4. Item 35: Familiarize yourself with °the language standard.

  • Item 35: 언어 표준에 친해져라.
출반된 1990년 이후, The Annotated C++ Reference Manual은 프로그래머들에게 C++에 정의에 참고문서가 되어 왔다. ARM기 나온 이후에 몇년간 ISO/ANSI committe standardizing the language 는 크고 작게 변해 왔다. 이제 C++의 참고 문서로 ARM은 더 이상 만족될수 없다.

post-ARM 은 C++로 좋은 프로그램을 작성 할수 있느냐에 큰 영향을 준다. 결과적으로 C++ 프로그래머들에게 ARM의 내용과 다른 표준 사항을 아는 것은 프로그래머들에게 첫번째로 중요한 문제가 될것이다.

ISO/ANSI standard for C++ 는 컴파일러가 구현될때, 벤더들의 조언이고, 책을 준비할때, 작가들이 시험해 볼 것이고, 프로그래머들이 C++에 관한 정의에 질문에 답이다. ARM이 나온 이후 C++에 가장 큰 변화를 알아 보자.
  • 새로운 개념의 추가 : RTTI, namespace, bool, mutable과 explicit keyword, enum을 위한 오벌드(overload) 연산자 능력, 클래스 정의 내부에서 이용한 완전한 정적 클래스 멤버 초기화 증력
  • 템플릿(template)의 확장 :멤버 템플릿이 허용. 이것은 탬플릿의 명시적 표현을 위한 표준 문법,함수 템플릿에서 non-type 인자들 허용 하는것, 클래스 템플릿이 그들 자신의 템플릿을 인자로 받을수 있는것 이 있다.
  • 예외 핸들링의 재정의 : 예외 스팩은 현재 컴파일 중에 더욱더 엄격하게 검사 된다. 그리고 예측할수 없는 함수는 아마도 bad_exception 예외 객체를 던진다.
  • 메모리 할당 루틴의 수정 : operator new[]와 operator delete[] 가 추가, operator new/new[] 는 이제 메모라가 할당 되지 않으면, 예외를 던지다. 그리고 이 바뀐 operator new/new[]는 실패할 경우에 0 을 반환한다.
  • 새로운 케스팅 연산자의 추가 : static_cast, dynamic_cast, const_cast, reinterpret_cast
  • 언어 규칙의 개선 : 가상 함수의 개선으로 이제 더이상 재정의한 함수와 정확히 일치하는 형의 반환인자를 가지지 않는다. 그리고 임시 객체의 생명주기도 정확하게 정의되었다.
The Design and Evolution of C++에 거의 모든 변화가 언급되어 있다. 현재 C++ 참고서(1994년 이후에 쓰여진것들)도 이 내용을 포함하고 있을 것이다. (만약 당신이 찾지 못하면 그거 버려라) 덧붙여 More Effective C++(이책이다.)는 이러한 새로운 부분에 관한 대부분의 사용 방법이 언급되어 있다. 만약 당신이 이것에 리스트를 알고 싶다면, 이 책의 인덱스를 살펴보아라.

표준 라이브러리에 일어나는 것들에 대한것에서 C++의 정확한 규정의 변화가 있다. 개다가 표준 라이브러리의 개선은 언어의 표준 만큼이나 알려지지 않는다. 예를 들어서 The Design and Evolution of C++ 거의 표준 라이브러리에 관한 언급이 거의 없다. 그 책의 라이브러리에 과한 논의라면 때때로 있는 데이터의 출력이다. 왜냐하면, 라이브러리는 1994년에 실질적으로 변화되었기 때문이다.

표준 라이브러리의 능력은 아래와 같은 일반적인 카테고리에 대하여

  • 표준 C 라이브러리에 대한 지원 C++ 가 그것의 뿌리를 기억한다면, 두려워 할것이 아니다. 몇몇 minor tweaks는 C라이브러리의 C++ 버전을 C++의 더 엄격한 형 검사를 도입하자고 제안한다. 그렇지만 모든 개념이나, 목적을 위해, C 라이브러리에 관하여 당신이 알아야 하는 좋와(혹은 싫어)해야 할것은 C++에서도 역시 유지된다.
  • 문자열에 대한 지원. 표준 C++ 라이브러리를 위한 워킹 그룹의 수석인 Mike Vilot은 이렇게 이야기 했다. "만약 표준 string 형이 존제하지 않는다면 길거리에서 피를 흘리게 될것이다.!" (몇몇 사람은 매우 감정적이었다.) 진정하라. - 표준 C++ 라이브러리는 문자열을 가지고 있다.
  • 지역화(localization)에 대한 지원. 다른 문화에서는 다른 글자를 써야하고, 화면에 표현되는 날짜, 시간, 문자열 정렬, 돈을 세는 단위 etc 그 문화의 편의에 따라야 한다. 표준 라이브러리에 의한 지역화(localization)은 각 문화적 차이에 적합하도록 프로그램의 개발을 한다.
  • I/O에 대한 지원. iostream 라이브러리는 C++ 표준의 한 부분을 차지한다. 하지만 위원회는 그걸 좀 어설프게 만들었다. 몇몇 클래스가 제거되고(필요 없는 iostream과 fstream), 몇몇 클래스가 교체(string 기반의 stringstream은 char* 기반으로) 되었지만, 표준 iostream 클래스의 기본 능력은 몇년 동안 존재해온 (옛날) 구현의 모습을 반영한다.
  • 수치 계산 어플리 케이션에 대한 지원. 복잡한 숫자, C++ 텍스트에 중심 예제로 마지막에 표준 라이브러리에 포함되었다. 더불어, 라이브러리는 aliasing을 제한하는 특별한 배열 클래스(valarray)가 포함되어 있다. 라이브러리는 또한 몇가지의 일반적인 유용한 수치 계산의 함수들, 부분적 합과 인접하는 이웃의 차이를 포함하는 것들, 을 지원한다.
  • 일반적인 목적의 컨테이너와 알고리즘에 대한 지원. 표준 C++ 라이브러리에 포함되어 있는 클래스, 함수 템플릿은 Standard Template Library(STL)로 알려져 있다. STL은 표준 C++ 라이브러리의 가장 혁명적인 부분이다. 나는 밑에 이것의 특징을 요약한다.

내가 STL을 언급하기 전에, 나는 반드시 당신이 알기에 필요한 C++ 라이브러리의 두가지의 특징을 이야기 해야한다.

첫번째 라이브러리 안의 거의 모든것이 template이다. 이 책 내에서 나는 아마도 표준 string 클래스를 참고 했다. 그러나 사실 그런 클래스가 아니다. 대신에 문자들의 순서를 표현하는 basic_string 으로 불리는 클래스 템플릿이고, 이 템플릿은 문자형으로 순서를 만든다. 이것은 문자열을 char, wide char, Unide char, 무엇이든 허용한다.

우리가 일반적으로 생각하는 string 클래스는 basic_string<char>을 명시적으로 표현한 것이다. 이것을 일반화 시키기 위하여 표준 라이브러리에서는 다음과 같이 정의하고 있다.

~cpp 
typedef basic_string<char> string;
basic_string 템플릿이 세가지의 논의를 가지고 아서, 이 한줄에 조차 자세하게 주석을 달수 있다.;그렇지만 무엇보다 먼저 생각할것 첫번째는 기본 값이다. 진짜 string 형의 이해를 위해서 반드시 이와 같은 상황에 직면하는데, 바로 basic_string에 대한 선언을 수정할수 없는가? 이다.


~cpp 
template<class charT,
        class traits = string_char_traits<charT>,
        class Allocator = allocator>
    class basic_string;

string 형의 사용을 위하여 위의 사항을 완전히 이해할 필요는 없다. 왜냐하면 단지 string은 Template Instantiation from Hell을 위한 typedef 이지만, 그것은 템플릿이 아닌 클래스와 같이 동작한다. 단지, 만약 당신이 문자열을 이루는 문자 형의 custmize가 필요하다면.. 혹은 당신이 문자열을 위한 메모리 할당에 대한 세부적인 조정을 원한다면... basic_string 템플릿은 이들을 할게 해줄것이라는 생각을 마음속에 새겨두어라.

string 형의 디자인에 반영된 이러한 접근은-템플릿의 일반화- 표준 C++ 라이브러리를 통해서 반복되어 진다. IOStream? 그들은 템플릿이다.; 인자(type parameter)는 스트림에서 만들어지는 문자형으로 정의되어 있다. 복잡한 숫자(Complex number)? 역시 템플릿이다.;인자(type parameter)는 숫자를 어떻게 저장할지 정의되어 있다. Valarray? 템플릿이다.;인자(type parameter)는 각 배열에 최적화된다. 그리고 STL은 거의 모든 템플릿의 복합체이다. 만약 당신이 템플릿에 익숙하지 않다면, 지금 한발작 내디뎌 보아라.

표준 라이브러에 관하여 다른 부분도 std 이름 공간안에 모든것이 가상적으로 포함되어 있다. 표준 라이브리에서 명시적으로 그들의 이름을 표현하지 않고 쓰기 위해서는, using으로 직접 접근을 하거나 미리 using선언을 통해 접근할수 있다. (DeleteMe 모호)다행스럽게도 이러한 문법의 관리는 당신이 #include 를 사용할때마다 알맞는 헤더에 적용된다.

Fortunately, this syntactic administrivia is automatically taken care of when you #include the appropriate headers.

4.1. The Standard Template Library : 표준 템플릿 라이브러리


표준 C++ 라이브러리에서 가장 큰 뉴스는 Standard Template Library(표준 템플릿 라이브러리)인 STL이다. (C++ 라이브러리에서 거의 모든것이 템플릿이 된이후 그 이름 STL은 이제 특별한것이 아니다. 그럼에도, 이것은 라이브러리의 알고리즘과 컨테이너의 부분의 이름이다. 그래서 쓰기에 좋은 이름이기도, 나쁜 이름이기도 한다.)

STL은 많은 조직-거의 모든 C++라이브러리-에 영향을 미치는 것 같다. 그래서 그것의 일반적인 개념과 친해지는 것은 매우 중요하다. 그들은 이해하기는 어렵지 않다. STL은 세가지의 기본적인 개념에 기반하고 있다.: container, iterator, algorithm. Container는 객체의 모음(collection)이다. Iterator는 STL 컨테이너에서 당신이 built-in 형의 인자들을 포인터로 조정하는 것처럼 객체를 가리킨다. Algorithm은 STL container와 iterator를 사용해서 그들의 일을 돕는데 사용하는 함수이다.
배열을 위한 C++(그리고 C)의 규칙을 기억하는 것이 STl을 전체적으로 바라보는데 가장 선행되어야 할 작업이다. 우리가 알아야 할것은 정말 오직 하나의 규칙이다.:배열을 가리키는 포인터는 합법적으로 배열상의 어떠한 인자나 배열의 끝을 넘어 어떠한 인자라도 가리킬수 있다. 만약 포인터가 배열의 끝을 넘은 인자를 가리킨다면, 그것은 배열을 가리키는 다른 포인터와 비교 되어지는 셈이다.; 결과적으로 정의되지 않은 dereferencing을 수행하는 것이다.

위의 규칙은, 배열상에서 하나의 특별한 값을 찾기위한 함수의 작성의 규칙에 이점이 된다. integer(정수)의 배열에, 우리는 이와 같은 함수를 작성하였다.

~cpp 
int * find(int *begin, int *end, int value)
{
    while (begin != end && *begin != value) ++begin;
    return begin;
}
이 함수는 해당 목표 시점의 시작과 끝 상에 해당 값이 있는가를 찾는다. 그리고 배열상에 해당 적당한 수를 처음 만나면 반환한다.; 만약 찾지 못하면, end를 반환한다.

end를 반환하는 것은 단순한 평범한 방법으로 재미있게 보인다. 0(null pointer)이 더 좋지 않을까? 확실히 null은 더 자연스럽게 보이겠지만 "더 좋지"는 않다. 찾는 함수는 반드시 검색이 실패함을 의미하는 어떠한 뚜렷한 포인터 값을 반환해야만 한다. 그런 목적으로 end 포인터는 null만큼 좋다. 거기에다가 우리가 이제 곧 보게될, null 포인터 보다 다른 container 형으로 일반화 시키는 것을 알수 있다.

솔찍히 이것은 아마 당신이 찾기 함수(find function)를 작성한 방법이 아니다. 그렇지만 결코 멍청한 것이 아니고, 그것은 매우 훌륭히 일반화 된다. 당신이 이와 같은 간단한 예제를 따르면, STL에서 찾을수 있는 대다수의 생각들을 가지고 있는거다.

당신은 찾기 함수(find function)를 다음과 같이 사용할수 있다.

~cpp 
int values[50];
...

int *firstFive = find(  values,     // 숫자 5를
                        values+50,  // value[0] - value[49]
                        5);         // 까지 검색

if (firstFive != values+50) {       // 검색이 성공했나요?
    ...                             // 네
}
else {
    ...                             // 아니요. 검색에 실패 했다.
}

당신은 배열의 subrange 검색도 할수 있다.

~cpp 
int *firstFive = find(values,        // search the range
                      values+10,     // values[0] - values[9]
                      5);            // for the value 5

int age = 36;

...

int *firstValue = find(values+10,    // search the range
                       values+20,    // values[10] - values[19]
                       age);         // for the value in age

해당 함수는 배열의 적용이 int에 한정되어 있어서 상속성이 없다. 그래서 그것을 템플릿(template)으로 만들어 본다.


~cpp 
template<class T>
T * find(T *begin, T *end, const T& value)
{
    while (begin != end && *begin != value) ++begin;
    return begin;
}

템플릿으로의 변환에서 값으로의 전달을 reference to const 로 전달로 변화시킬 방법이 없음을 주목해라. 각 값으로의 인자 전달은 우리에게 매번 객체의 생성과파괴의 비용을 지불하게 만든다. 우리는 pass-by-reference를 사용해서, 아무런 객체의 생성과 파괴를 하지 않도록 만들어서 해당 비용을 피해야 한다.

그렇지만 이 템플릿은 좋다, 개다가 일반화 까지 할수 있다. 시작과 끝에 연산자를 보아라. 사용된 연산자는 다르다는 것, dereferencing, prefix증가(Item 6참고), 복사(함수의 반환 값을 위해서? Item 9참고)를 위해서 쓰였다. 모든 연산자는 우리가 overload할수 있다. (DeleteMe 모호) 그래서 왜 포인터 사용하여 찾기를 제한하는가? 왜 허용하지 않는가 포인터 추가에서 이러한 연산자를 지원하기 위한 어떤 객체도 허용할수 없을까? (hy not allow any object that supports these operations to be used in addition to pointers?) 그래서 이렇게 하는것은 포인터 연산자의 built-in 의미를 찾기함수(find function)을 자유롭게 할것이다. 예를 들어서 우리는 리스트 에서 다음 리스트로의 이동을 해주는 prefix increment 연산자의 linked list객체와 비슷한 pointer를 정의할수 있다.

이것이 STL iterator 뒤에 숨겨진 개념이다. Iterator는 STL container가 사용하기 위하여 pointer 비슷하게 설계된 객체이다. 그들은 Item 28의 스마트 포인터의 사촌격인 셈이다. 그렇지만 스마트 포인터는 STL의 iterator가 하는 것에 비하여 조금더 모호한 경향이 있다. 그렇지만 기술적 관점에서 그들은 같은 기술을 이용해서 구현된다.

pointer 비슷한 객체로서 iterator, 우리는 iterator를 이용한 검색에서 포인터를 교체할수 있다. 그래서 다음과 같이 코드를 다시 쓸수있다.


~cpp 
template<class Iterator, class T>
Iterator find(Iterator begin, Iterator end, const T& value)
{
  while (begin != end && *begin != value) ++begin;
  return begin;
}

축하한다! 당신은 Standard Template Library의 한 부분을 작성한 것이다. STL은 container와 iterator를 이용하는 알고리즘 묶음을 포함하고 있다. 그리고 find는 그들중에 하나이다.

STL에서 container는 bitset, vector, list, deque, queue, priority_queue, stack, set, map 포함한다. 그리고 당신은 이러한 어떤 container 형을 find에 적용할수 있다.


~cpp 
list<char> charList;        // char을 수용하는 
                            // STL list객체를 생성한다.                            
...

// char list에서 'x' 가 처음 나타나는 부분을 찾는다.
list<char>::iterator it = find(charList.begin(),
                               charList.end(),
                               'x');

"와우!" 나는 당신의 울부짓음이 들리는걸, "이것은 위의 배열 예제와 다른게 하나도 없잖아요!" 아, 그러나 그건:당신은 찾기위한 것만을 알고 있기만 하면 된다.

list 객체를 찾기 위한 호출을 위해서, 당신은 list의 가장 처음 인자와 list와 가장 마지막의 인자를 가리키는 iterator가 필요하다. 리크트 클래스에 의한 몇가지의 도움 될 기능들을 제외하고, 이것은 어려운 문제이다. 왜냐하면 당신은 list가 어떻게 구현되었는 가에 관한 정보가 없기 때문이다. 다행 스럽게다 리스크(list,모든 STL의 container들과 같이) 시작과 끝을 제공하는 멤버 함수로서 해결한다. 이 멤버 함수는 당신이 원하는 iterator 반환하고 위의 예제에서 해당 iterator 두가지를 찾을수 있다.

찾기(find)가 끝났으면 그것은 찾은 인자를 가리키는, 혹은 charList.end()(찾지 못하였을때)의 iterator 객체를 반환한다. 왜냐하면 당신은 list가 어떻게 구현되었는가에 대하여 아무것도 알수 없기때문에 역시 구현된 list에서 iterator가 어떻게 되었는지 전혀 알수 없다. 그런데 어떻게 찾고서 반환되어지는 객체의 형을 알수 있을까? 다시, list 클래스는 모든 STL container와 마찬가지로 제한 해제를 한다.: 그것은 형 정의, iterator, 즉 list내부의 iterator의 정의를 제공한다. charList 가 char의 list가 된 이후로 iterator의 정의는 그러한 list인 list<char>::iterator 내부에 있고, 그것은 위의 예제와 같이 쓰인다. (각 STL container 클래스는 두가지의 iteraotr형인 iteratorconst_iteraotr 를 정의한다. 전자는 일반적인 pointer와 같이 동작하고 후자는 pointer-to-const와 같이 동작한다. )

다른 STL continer들을 이용해서 정확히 같은 접근을 해본다. 게다가 C++ 포인터는 STL iterator이다. 그래서 원래 배열 예제는 STL find 함수도 역시 이용할수 있다.

~cpp 
int values[50];

...

int *firstFive =   find(values, values+50, 5);  // 맞다. STL find를 호출한다.

STL, 그것의 중심(core)는 매우 간단하다. 그것은 단지, 대표 세트(set of convention)를(일반화 시켰다는 의미) 덧붙인 클래스와 함수 템플릿의 모음이다. STL collection 클래스는 클래스로 정의되어진 형의 iterator 객체 begin과 end 같은 함수를 제공한다. STL algorithm 함수는 STL collection상의 iterator 객체를 이동시킨다. STL iterator는 포인터와 같이 동작한다. 그것은 정말로 모든 것이 포인터 같다. 큰 상속 관계도 없고 가상 함수도 없고 그러한 것들이 없다. 단지 몇개의 클래스와 함수 템플릿과 이들을 위한 작성된 모든 것이다.

또 다른 면을 말한다.: STL은 확장성이 있다. 당신은 당신의 collection, algorithms, iterator를 STL에 추가할수 있다. 당신이 STL 협의를 따르는 이상 표준 STL collection은 아마도 당신의 algorithm과 당신의 collection은 STL의 algorithms과 함깨 동작할 것이다. 물론 당신의 템플릿은 표준 C++ 라이브러리의 한부분이 아니다. 그렇지만 그들은 같은 원리로 만들어 질것이고, 재사용 될것이다.

내가 여기에 기술한 것보다 훨썬 많은 C++ 라이브러리가 존재한다. 당신은 라이브러리를 효과적으로 쓰기 전에, 반드시 당신이 알고 있는 요약 보다 더 많은 것을 배워야 하고, 당신의 STL-compliant 템플릿을 작성하기 전에, 반드시 STL의 협의에 관해서 공부해야 한다. 표준 C++ 라이브러리는 C 라이브러리 보다 훨씬 풍부하다. 아마 당신 자신이 그것(C++lib)에 친숙할수록 시간은 절약 될것이다. 게다가 라이브러리의 디자인 철학은 - 일반화(generality), 확장성(extensibility), 최적화(customizability), 효율성(efficiency), 재사용성(reusability) - 당신이 올바른 배움으로 가는데 매우 도움이 된다. 표준 C++ 라이브러리의 공부로 당신은 당신의 소프트웨어에서 사용 가능한 컴포넌트를 만드는 재반 지식의 향상은 물론, C++ 더 효율적으로 C++의 특징을 적용시키는 방법을 배운다. 그리고 당신은 당신이 가진 라이브러리의 좋은 디자인의 방법에 대한 해안을 얻는다.

Valid XHTML 1.0! Valid CSS! powered by MoniWiki
last modified 2021-02-07 05:23:48
Processing time 0.2004 sec