U E D R , A S I H C RSS

More EffectiveC++/Techniques3of3

No older revisions available

No older revisions available



1. Item 31: Making functions virtual with respect to more than one object.

  • Item 31: 하나 이상 객체에 대응하는 함수를 virtual(가상)으로 동작 시키기
DeleteMe 모호

Sometimes, to borrow a phrase from Jacquelin Sussan, once is not enugh. (의역:제클린 수잔에게서 말을 빌려 유식 한척하는데 한계가 있다. 정도. 이해안감)

예를들어 생각해 보자. 당신은 Redmond, Washington -- Nintendo를 의미 -- 라는 유명한 소프트웨어 회사에서 높은 profile(인물평), prestige(지위), paing(임금)를 가진 프로그래밍 직업을 가지기 노력한다. Nintendo에서 결정권을 가진 지위를 당신이 가진다면, 당신은 비디오 게임에 관한 작성에 대한 결정을 해야 한다. 그러한 게임은 우주에서 space ship(우주 비행선:이하 space ship)을 운영하고, space station(우주 비행장,우주 항공모함:이하 space station)과, asteroid(소행성:이하 asteroid)이 등장한다고 해보자.

ship과 station, asteroid는 당신의 세상에서 날아다니므로, 그들은 자연스럽게 서로 부딪치기도 한다. 자, 이러한 충돌에 관한 규칙을 정의하자
  • ship과 station이 느린 속도에서 충돌한다면, ship은 station에 안착한다. 반면에 ship과 station은 충돌하는 속도에 비례하여, 피해를 입는다.
  • ship과 ship이나 station과 station의 충돌이 있으면, 충돌상에서 양쪽다 피해를 입으며, 피해양은 충돌시 속도에 비례 한다.
  • 만약 소행성(small asteroid)이 ship이나 station과 충돌했다면, asteroid는 파괴된다. 만약 큰 행성(big asteroid)와 충돌이라면, ship과 station은 파괴된다.
  • asteroid와 다른 asteroid가 충돌한다면 양쪽다 파괴되어 조각나고, 모든 방향으로 작은 asteroid가 흩어진다.

이것은 그리 좋은 게임이 아닌것 같이 들리지만, 여기에서 우리가 취할 목적은 객체들 사이의 충돌 검출하는 C++코드에 관한 방법을 생각해 보는것이다.

일단, ship, station, asteroid이 공유하는 면이 전혀 없다고 생각하고 시작한다. 없다면 그들의 모두, 움직임을 표현하기 위한 속도를 가진다. 실제로 그러한 클래스들은 거의 추상 기초 클래스(abstract base class)가 불분명하다. 만약 Item 33에서 처럼, 기초 클래스는 추상이 좋다.(33에서는 모두 추상이다.) 암튼, 이들 객체으 관계를 정리한 상속도를 본다면 다음과 같다.



~cpp 
class GameObject { ... };
class SpaceShip: public GameObject { ... };
class SpaceStation: public GameObject { ... };
class Asteroid: public GameObject { ... };
아제 이 프로그램에서 객체들의 충돌 검출에 대한 코드 작성에 대하여 깊게 생각해 보자. 당신은 다음과 같이 보이는 함수로 충돌 검출을 수행한다.

~cpp 
void checkForCollision(GameObject& object1, GameObject& object2)
{
    if (theyJustCollided(object1, object2)) {
        processCollision(object1, object2);
    }
    else {
        ...
    }
}
이는 프로그램의 역할을 명확하게 해준다. processCollision이 호출때, object1과 object2가 충돌한다는 것과, 이 충돌의 영향이 object1과 object2에게 상호 의존적이라는 것을 알수 있다. 그렇지만 문제는 이 객체의 종류를 알수 없다는 점이다.;둘다 알다시피 GameObject라고 명시되어 있다. 만약 충돌의 과정이 object1의 동적형에 의존한다면 당신은 GameObject내의 가상함수로 되어 있는, processCollision를 호출해서 처리해야하고, object1.processCollision(object2) 이런식의 호출을해야 한다. 역시나 object2에 의존적이고, 동적이라면 같은 방식으로 처리되어야 한다. 그렇지만 충돌시 처리해야 할 수행과정은 충돌 객체의 양쪽 모두의 동적 형에 의존하고 있어서, 둘의 정보를 다 알고 있어야 한다. 함수는 그들중 하나의 가상 함수만을 부를수있다. 보다시피 확실한 해결책이 아니다.

필요로 하는것이 하나 객체이상의 형에 대한 가상함수의 동작이다. C++은 그러한 함수를 제공하지 않는다. 하지만 이것을 구현해야 하는데, 그 질문을 어떻게 해결할수 있는지 생각해 보자.

하나의 가능성은 C++의 사용을 줄이고, 다른 프로그래밍 언어를 선택하는 것이다. 예를들어, CLOS(Common Lisp Object System)로 전환 할수 있다. CLOS의 가장 일반적인 객체지향 함수 호출 기술의 하나(general object-oriented function-invocation mechanism one)로 해결이 가능하다.:가중 메소드(multi-method)기능. 다중 메소드(multi-method)는 당신이 원하는 여러개의 인자들로서 virtual(가상)을 구성할수 있고, CLOS는 당신의 조정으로 multi-method들의 오버로드(overload) 통한 방식으로 해결책을 준다.


~cpp 
dispatch [출발시키다]
다중 프로그래밍 시스템에서 다음에 처리될 작업을 선택하여 실행시키는 것. 즉, 대기 열에서 기다리고 
있는 프로세스를 선택하여 중앙 처리 장치의 사용 권한을 부여하는 작업. 
출처:한메 myQuickFind 컴용어 사전

그렇지만 C++안에서만 당신의 게임을 구현해야 한다고 하자-이제 당신은 보통 double-dispatch 으로 불리는 기술에 대해 구현하는 방법을 찾아야 한다. (이 이름은 객체지향 프로그래밍 커뮤니티(object-oriented programming community)에서 C++의 가상함수를 "message dispatch"으로 부르는 C++프로그래머들에 의해서 불린다. 이 호출은 두개의 인자가 가상으로 동작하기위해 "double-dispatch"를 통하여 구현된다. 이것을 일반화 시키면,-함수는 가상으로 몇개의 인자를 가진다.- multi-dispatch 라고 부를수 있다.) 이번 아이템에서는 이를 구현하는데, 몇가지의 접근법을 다룰 것이다. 약점이 없는것이 없고, 그리 당신을 놀래킬 방법들도 아니다. C++는 double-dispat에 관한 직접적 지원을 안해서 반드시 가상함수 정도의 지원을 컴파일러가 해주어야 한다.(Item 24참고) 만약 컴파일러가 지원하면, 아마 우린 C에서 처럼 간단히 프로그래밍을 할것이고, 아니라면 각오해야 한다.

1.1. Using Virtual Function and RTTI : 가상 함수와 RTTI사용

가상 함수는 single-dispatch로 구현된다. 이것은 우리가 구현하는 것에 반쪽에 불과하다. 그리고 컴파일러는, 이런 가상함수를 지원할수 있다. 그래서 가상함수인 collid를 GameObject내에 선언하는 것으로 시작해 보자. 이 함수는 유도되는 클래스에 의하여 오버라이드(overridden)된다.

~cpp 
class GameObject {
public:
    virtual void collide(GameObject& otherObject) = 0;
    ...
};

class SpaceShip: public GameObject {
public:
    virtual void collide(GameObject& otherObject);
    ...
};
여기서는 SpaceShip 클래스만이 유도했지만, SpaceStation과 Asteroid도 같은 방법으로 정의된다.

double-dispatch에 관한 가장 일반적인 접근 방법은, 허용할수 없는 방법이겠지만(unforgiving world), if-then-else의 연쇄 관계를 이용해서 가상 함수를 흉내내도록 만드는 것이다. 이것을 약간 더 현실적으로 한다면(this harsh world) 먼저 otherObject에 대한 진짜 형을 찾고, 다음에 모든 가능성에 대한 접근을 해보는 것이다.

~cpp 
// 충돌 객체의 종류를 알수가 없다면, 예외를 던진다.
class CollisionWithUnknownObject {  // 던질 예외 객체
public:
    CollisionWithUnknownObject(GameObject& whatWeHit);
    ...
};

void SpaceShip::collide(GameObject& otherObject)
{
    const type_info& objectType = typeid(otherObject);  // 충돌의 형을 알아 낸다.

    if (objectType == typeid(SpaceShip)) {
        SpaceShip& ss = static_cast<SpaceShip&>(otherObject);

        SpaceShip-SpaceShip 간의 충돌 수행;
    }
    else if (objectType == typeid(SpaceStation)) {
        SpaceStation& ss = static_cast<SpaceStation&>(otherObject);

        SpaceShip-SpaceStation 간의 충돌 수행;
    }
    else if (objectType == typeid(Asteroid)) {
        Asteroid& a = static_cast<Asteroid&>(otherObject);

        SpaceShip-Asteroid 간의 충돌 수행;
    }
    else {
        throw CollisionWithUnknownObject(otherObject);
    }
}
여기에서 우리가 필요한 객체의 형을 판단하는 방법에 주목하라 다른 객체는 *this이고, 이는 가상 함수 구조에 의해 결정되어 진다. 우린 SpaceShip 멤버 함수 내부에 있다. 그래서 *this는 반드시 SpaceShip객체이고 그래서 오직 otherObject의 종류만 밝혀내면 된다.

이와 같은 코드는 복잡하지 않다. 오히려 쓰기 쉽다. 수행하기도 수월하다. 오히려 RTTI가 걱정이 된다.:하지만 그 부분도 그리 잘못되어 보이지 않다. 정말 문제는 마지막 else에서 일어나는 예외를 던지는 부분이다.

이러한 코드들은 캡슐화와 이별을 고한다. 왜냐하면 collide 함수는 그것의 모든 동급의 클래스-GameObject에서 상속된 클래스-들을 알고 있어야만 한다. 만약 새로운 형의 객체-새로운 클래스-가 게임에 추가된다면, 당신은 각각의 RTTI에 근거하여 if-then-else 체인을 프로그램 내부에 추가시켜야 할것이다. 만약 우리가 이것중 하나라도 까먹는다면, 프로그램은 모호한 버그를 가지게 된다. 게다가 컴파일러는 우리에게 이런 것을 찾는것에 관한 어떠한 도움도 지주 못한다.

형기반의 프로그래밍이라면 C부터 있었던 오래된 개념이다. 그리고 우리가 알게 된것들중 하나는 근본적으로 유지 보수 하기에 너무 어렵다는 문제를 야기한다. 그런 프로그램이 점차 발전한다는 것은 아마 생각하기 힘들것이다. 이는 가상 함수가 등장하게된 첫번째 이유이다.:이유-형 기반의 함수를 유지 보수 하고 만드는 짐을 프로그래머에서 컴파일러로 넘긴다. RTTI가 double-dispatch 구현에 채용되면, 이러한 오래된 고민들을 다시 하는것이다.

C에너 논의된 에러의 가능성을 가진 이런 유산들은 C++에서도 역시 에러로 이끈다. 실수하기 쉬운 경우는, collide함수의 마지막 else 부분이다. 이 잘못은 만약 알수없는 객체의 경우 프로그램의 제어를 날려버린다. 이런 경우에는 원칙적으로 불가능 하지만, 우리가 RTTI 사용을 생각할때, 어디에 핵심이 있는가? 거기에는 다양한 방법의 많은 상호 작용이 있을수 있다. 아무도 완전히 만족 시킬수 없다. 이러한 경우에, 예외를 던지도록 선택한다. 그렇지만 함수의 호출자들은 이 에러를 확실히 잡아 해결할 방법 역시 명백하지 못하다.

1.2. Using Virtual Functions Only : 가상 함수만 사용

double-dispatch의 구현을 위해서 RTTI로의 접근할때,상속의 위험을 최소화하는 방법이 있다.(제일 처음꺼) 그렇지만 우리가 이전에 보았듯이, 특별히 아무것도 사용하지 않고, 변한 해결첵을 제시하지만 가상 함수이다. 이러한 적략 역시 RTTI가 접근하는 방식과 동일한 개념에서 시작하는 것이다. collide 함수는 GameObject의 가상 함수로 선언되고, 각 유도된 클래스에서 재 정의 되는 것이다. 거기에다가 각 클래스에다가 collide를 유도된 클래스의 구조에 따라 오버로드(overload)해버리는 것이다.

~cpp 
class SpaceShip;                        // 이름 선언
class SpaceStation;
class Asteroid;

class GameObject {
public:
    virtual void collide(GameObject&      otherObject) = 0;
    virtual void collide(SpaceShip&       otherObject) = 0;
    virtual void collide(SpaceStation&    otherObject) = 0;
    virtual void collide(Asteroid&        otherobject) = 0;
    ...
};

class SpaceShip: public GameObject {
public:
    virtual void collide(GameObject&       otherObject);
    virtual void collide(SpaceShip&        otherObject);
    virtual void collide(SpaceStation&     otherObject);
    virtual void collide(Asteroid&         otherobject);
    ...
};
기본 생각은 double-dispatch을 두개의 단일 dispatch로 구현하는 것이다. 다시 말해, 두개의 각기 다른 가상함수를 호출하는 것이다.; 첫번째로 동적 형의 첫번째 객체를 결정한다. 두번째로 두번째 객체를 결정한다. 이것을 하기전 전에, 처음 가상 함수의 호출의 collide 함수가 GameObject& 인자를 가진다. 해당 함수는 다음과 같이 간단히 구현된다.:

~cpp 
void SpaceShip::collide(GameObject& otherObject)
{
    otherObject.collide(*this);
}
처음 주목할것은 이것이 collide를 다시 부르는것 같이 보이는 것으로, 재귀적 호출과 다를바 없이 보인다는 점이다.다시 말하자면, otherObject는 멤버 함수를 호출하고, 이 호출인자를 *this가 들어가는 점. 하지만 다시 보면 이것은 결코 재귀적 호출이 아니다. 알다 시피 컴파일러는 호출하기 위한 함수들이 들어가는 인자들을 정적 형(static type)으로 계산한다. 이런 경우에 있어서, 4가지의 다른 collide 함수가 불려 질수 있지만, *this의 정적 형(static type)에 의하여 선택된다. 그 정적 형은 무엇인가? SpaceShip클래스의 맴버 함수라면 *this는 SpaceShip이 된다. 이 호출은 그래서, GameObject&형을 인자로 가지는 함수가 아닌, SpaceShip&의 형을 인자로 가지는 함수가 호출된다.

모든 collide함수는 가상이다. 그래서 SpaceShip::collide 내부에서 otherObject의 진자 형과 반응할수 있는 collide의 구현이 가능해 진다. 그런 collide 구현의 내부에는, 양쪽 객체의 실제 형에 관해서 알수 있다. 왜냐하면, left-hand 객체가 *this이기 때문이다.(목표한 형에서 구현된 멤버 함수 이다.) 그리고 right-hand 객체의 실제 형은 SpaceShip로, 인자의 선언된 형과 같다.

이번 구현 코드를 봐서 모든것이 더 명백해 질것이다.

~cpp 
void SpaceShip::collide(SpaceShip& otherObject)
{
    SpaceShip-SpaceShip 간의 충돌을 수행한다.;
}
void SpaceShip::collide(SpaceStation& otherObject)
{
    SpaceShip-SpaceStation 간의 충돌을 수행한다.;
}
void SpaceShip::collide(Asteroid& otherObject)
{
    SpaceShip-Asteroid 간의 충돌을 수행한다.;
}
보는 것과 같이, 여기에는 아무론 혼론이 될만한 요소가 없다. 그리고, 예상못한 객체에 대한 예외를 던지는 코드 역시 없다. 여기에는 예상못할 객체 형은 있을수가 없기 때문이다. -여기에는 가상 함수를 이용한다는 것이 핵심이다. 사실 그렇게 치명적인 점이 없고, double-dispatch 문제에 최적의 해결책이 될것이다. (작성자주:예상 못할꺼 없으니까.)

문제는, 우리가 앞서 본대로 RTTI를 접근해서 해결한다는 점이다.:각 클래스는 역시 그것의 형제 클래스들에 관해서 반드시 알고 있어야 한다. 새로운 클래스가 추가되면, 코드는 반드시 추가되어야 한다. 그렇지만, 이런 경우에는 다른 점은 코드가 반드시 업데이트 되어야 한다는 점이다. if-then-else의 경우에는 수정되지 않아도 옳다. 하지만 종종 그래서 문제를 잃으킨다.:각 클래스가 정의될때 반드시 새로운 가상 함수들이 추가되어야 한다. 예를 들어서, 만약 당시이 새로운 클래스 Satelliete가 당신의 게임에 추가되었다면( GameObject를 상속 ), 프로그램상에 존재하는 각 클래스에 새로운 collide 함수를 추가해야 한다.

존재하는 클래스를 수정하는 것은 별로 좋지 못하다. 예를들어서, 당신이 스스로 전체의 비디오 게임 코드를 작성하는것 대신에, 비디오 게임 어플리 케이션 프레임 워크 내에서 off-the-shelf로서 코드를 조합해서 게임을 만들어 나간다고 한다면, 당신은 아마 GameObject클래스에 접근하는 코드나, 프레임 워크에서 유도되는 코드의 작성을 못할 것이다. 이러한 경우에, 새로운 맴버 함수나, 가상 혹은 다른 방법으로 생성할 방법이 없다. 대안으로, 당신은 수정을 요구하는, 클래스에 직접적인(physical) 접근을 해야 할것이다. 하지만 이러한 실질적인 (practical) 접근은 불가능하다. 예를들어서, 당신이 Nintendo에 고용되었고, 프로그램에서 사용하는 라이브러리에 GameObject와 다른 유용한 클래스들이 포함되어 있다고 가정하자. 확실히 당신은 GameObject의 단일 라이브러리의 사용만으로 프로그램을 만ㄷ르지 않을 것이다. 그래서 Nintendo는 아마 새로운 형의 객체가 프로그램에 추가될때마다 GameObject를 가진 라이브러리 만이 아닌, 전체를 재컴파일 해야 하는 더 심각한이 될것이다. 실질적으로 광범위하게 쓰이는 라이브러리를 수정하는 것은 흔하지 않다. 왜냐하면 모든걸 재 컴파일하는 비용이 너무 크기 때문이다.

DeleteMe 모호

길든, 짧든 간에, 만약 당신이 double-dispatch을 구현할때, 최고의 자원이라 할수 있는것은, 디자인 상에서 필요 없는 인자를 제거 할수 있도록 수정이 가능한 것이다. 그것을 제한다면, 그렇지 않았을때 보다. 가상 함수가 RTTI의 전략에 보다 좀더 안전하게 접근하는 방법일 것이다. 그렇지만 이 경우 당신의 시스템에 확장성을 고려한다면, 해더 파일들을 조작해야 할것이다. RTTI로의 접근은, 아무런 재 컴파일에 하지 않는 반면에, 위와 같은 구현은 소프트웨어의 유지 보수에 악영향을 미친다.당신은 아마 당신의 비용을 지불하고, 당신의 기회 조차 지불하게 된다.

1.3. Emulating Virtual Function Tables : 가상 함수 테이블 흉내내기

좀더 좋은 기회 가지는 방법이 있다. Item 24에서 컴파일러가 보통 가상함수를 함수 포인터(vtbl) 배열로 생성하고, 그후 가상함수가 불릴때 인덱스로 함수 호출을 구현하는 것을 기억하라. vtbl의 사용은 컴파일러가 if-then-else 같은 연산의 체인을 제거하는대 필요하고, 컴파일러가 모든 가상 함수에 대하여 비슷한 코드의 생성을 하도록 만든다.:정확한 vtbl 인텍스 결정, 그리고 함수의 호출은 vtbl의 위치에 달려있다.

당신도 이런걸 할수 없다는 근거는 없다. 이걸 구현한다면, RTTI만을 쓴거보다는 더 효율적으로 만들수 없다. (배열의 인덱싱과 함수 포인터를 허용하는것은 거의 if-then-else보다 더 효율적이다. 그리고 생성되는 코드 역시 마찬가지이다.), 또한 한곳에 RTTI 사용을 하는 것으로 제한한다.:함수 포인터가 초기화되는 곳.

DeleteMe 해석 보류 (I should mention that the meek may inherit the earth, but meek of heart may wish to take a few deep breaths before reading what follows.:구지 해석하면, 다른 사람을 쫓는 줏대없는 짓이라고 할지 모른다. 그렇지만, 다음 내용을 보기전까지는 뾰족한 수가 없을꺼다.) (작성자주:난 한국인이야!! - 자기 합리화)

자자, 몇가지 GameObject의 상속관계에서 함수들을 수정해서 시작한다.

~cpp 
class GameObject {
public:
    virtual void collide(GameObject& otherObject) = 0;
    ...
};
class SpaceShip: public GameObject {
public:
    virtual void collide(GameObject& otherObject);
    virtual void hitSpaceShip(SpaceShip& otherObject);
    virtual void hitSpaceStation(SpaceStation& otherObject);
    virtual void hitAsteroid(Asteroid& otherobject);
    ...
};
void SpaceShip::hitSpaceShip(SpaceShip& otherObject)
{
    SpaceShip-SpaceShip 간의 충돌 수행;
}
void SpaceShip::hitSpaceStation(SpaceStation& otherObject)
{
    SpaceShip-SpaceStation 간의 충돌 수행;
}
void SpaceShip::hitAsteroid(Asteroid& otherObject)
{
    SpaceShip-Asteroid 간의 충돌 수행;
}
RTTI 기반의 계층과 비슷한 모습으로 시작하는데, 우리는 GameObject 클래스가 충돌 과정에 대한 함수를 하나씩 가지고, 필요한 두가지의 dispatch의 수행을 하나에서 한다. 나중에 볼 가상함수 기반의 상속 관계와 비슷하게, 이건 경우의 함수는 collide로 이름을 공유하는 대신에 다른 이름을 사용하지만, 각 반응은 분리된 함수로서 캡슐화 된다. 이럴 경우, 오버로딩의 부재가 생각된다. 아마 곧 보여질 거다. 위의 디자인은 SpaceShip::collide를 위한 구현을 제외한 필요한 모든것을 포함하고 있다는 걸 살피자.;다양한 hit함수가 불린다. 그 전에, 일단 SpaceShip 클래스를 성공적으로 구현하고, SpaceStation과 Asteroid클래스도 적절히 구현해야 한다.

SpaceShip::collide 내부에 우리는 동적 형태의 인자인 otherObject를 그 형에 적합한 충돌체크를 하는 멤버 함수에 연결시켜야 하는 방법이 필요하다. 가장 쉬운 방법은 클래스 이름을 주어서 관련배열(associative array:이하 관련배열)을 생성하는 것으로, 함수 포인터 멤버에 올바르게 유도한다. collide에 이러한 관련배열(associative array)을 적용시켜 구현할수 있지만, 이해하기에 조금 더 쉬운 방법은 중재하는 함수인 lookup 함수를 GameObject에 추가하는 것이다. lookup은 적합한 멤버 함수 포인터를 반환한다. 그렇게 GameObject를 lookup에 넘기는 것과 반환되는 멤버 함수 포인터는 GameObject의 형에 충돌을 수행한다.

여기 lookup에 대한 선언이 있다.

~cpp 
class SpaceShip: public GameObject {
private:
    typedef void (SpaceShip::*HitFunctionPtr)(GameObject&);
    static HitFunctionPtr lookup(const GameObject& whatWeHit);
    ...
};
함수 포인터에 대한 문맥은 별로 좋와 보이지 않다, 그리고 멤버 함수 포인터는 평범하게 보이지도 않다. 그래서, HitFunctionPtr이라고 SpaceShipGameObject&를 인자로 가지고 아무것도 반환하지 않는, 멤버 함수들의 포인터를 줄여 형정의(typedef) 했다.

lookup을 가지고, collide의 한부분을 구현해 본다.

~cpp 
void SpaceShip::collide(GameObject& otherObject)
{
    HitFunctionPtr hfp = lookup(otherObject);   // 호출할 함수를 찾는다.

    if (hfp) {                              // 함수를 찾았으면 호출한다.
        (this->*hfp)(otherObject);
    }
    else {
        throw CollisionWithUnknownObject(otherObject);
    }
}
GameObject하에 클래스 계층으로 차례대로, 관련배열(associative array)들이 제공된다. 여기에서 lookup은 항상 처리 해야하는 객체를 위한 올바른 함수를 찾아야만 한다. 하지만 사람은 사람이다. 아무리 주의깊게 소프츠웨어 시스템에 신경을 써도 실수할수 있다. lookup에서 반환되는 포인터의 유효성을 검사해야 하는것이 그 이유이다.-"if(hfp)" 불가능한 상황에 대비하는 예외 처리역시 그 이유가 된다.

이제 남아있는건 lookup의 구현이다. 객체와 멤버 함수 포인터의 연관성있는 배열을 주어지면, lookup의 구현은 용이하다. 그렇지만 이 배열을 만들고, 초기화하고, 파괴하는 과정이 어려움으로 다가온다.

사용하기 전에 그러한 배열은 만들고 초기화 되어야 한다. 그리고 더이상 필요가 없다면 파괴되어야 한다. 이러한 배열을 손수 new와 delete를 사용해서 생성, 삭제 할수 있다. 하지만 잘못을 저지를 소지가 있다.:어떻게 배열의 사용전에, 초기화 된것을 보증할수 있을까? 좋은 방법은 컴파일러가 과정들을 자동화 시켜 버리고, lookup에서 관련있는 배열을 만드는 것으로 처리하는 것이다. 그 방법은 처음 lookup이 호출될때 초기화와, 생성을 할 것이고, main이후에 자동으로 종료 된다.

게다가 우리는 STL(Item 35참고) 에서 map을 사용해서 관련배열을 만들수 있다. 왜냐하면 map이 그런 역할을 하는것이기 때문이다.

~cpp 
class SpaceShip: public GameObject {
private:
    typedef void (SpaceShip::*HitFunctionPtr)(GameObject&);
    typedef map<string, HitFunctionPtr> HitMap;
    ...
};

SpaceShip::HitFunctionPtr
SpaceShip::lookup(const GameObject& whatWeHit)
{
    static HitMap collisionMap;
    ...
}
여기에서 collisionMap은 관련배열(associative array)이다. 그것은 클래스의 이름과(string 객체로) SpaceShip 멤버 함수 함수 포인터를 묶는다. 왜냐하면 map<string,HitFunctionPtr>로 하나로 묶였기 때문에, 이제 typedf을 이용해서 한번에 처리하기가 더 쉬워진다.(재미있게도 collisionMap의 선언은 HitMapHitFunctionPtr의 typedef 없이 한다. 대부분의 사람들이 이런식으로 처리를 원할 것이다.)

주어진 collisionMap으로 lookup의 구현은 다소 묘하게 바뀐다. map클래스가 지원하는 찾는 방식이 그러하다. 그리고 우리가 항상 typeid의 결과로 호출하는것은 name 이다.(예상할수 있겠지만, 객체의 동적 형태의 이름이다.) 그런데, lookup의 구현을 위해 우리는 collisionMap과 lookup의 구문에서 동적 형이 반응하는 엔트리를 찾으면 된다.

lookup은 코드는 여기 있다. 하지만 만약 STL이 친숙하지 않으면 Item 35를 참고하라 특별히 신경쓸것은 없다. 아마, 주석의 설명으로 알수 있을 것이다.

~cpp 
SpaceShip::HitFunctionPtr
SpaceShip::lookup(const GameObject& whatWeHit)
{
    static HitMap collisionMap;     // 초기화 관련은 다음 주제에서 살핀다.

    // 검색 결과의 iterator를 생성한다. (Item 35참고)
    HitMap::iterator mapEntry= collisionMap.find(typeid(whatWeHit).name());

    // 현재 map의 인자들이 있는가 살핀다.
    if (mapEntry == collisionMap.end()) return 0;

    // 짝지어진 함수 포인터를 반환한다.
    return (*mapEntry).second;
}
마지막 구문에서 (*mapEntry).second는 더 편한 mapEntry->second 대신에 표현된다. 이건 STL의 규칙이고, MEC++ 96 page, Item 18에서 잠시 언급했다.

1.4. Initializing Emulated Virtual Function Tables : 흉내낸 가상함수 테이블의 초기화


이제, collisionMap의 초기화가 남았다. 다음과 같은 모습으로 생각할수 있을 것이다.

~cpp 
// An incorrect implementation
SpaceShip::HitFunctionPtr
SpaceShip::lookup(const GameObject& whatWeHit)
{
    static HitMap collisionMap;

    collisionMap["SpaceShip"] = &hitSpaceShip;
    collisionMap["SpaceStation"] = &hitSpaceStation;
    collisionMap["Asteroid"] = &hitAsteroid;

    ...
}
하지만 이러할 경우에 lookup이 호출될때마다 해당 포인터들이 계속 추가되어 비효율 적이다. 더군다나 이건 컴파일 할수도 없다.

지금 필요한것은 collisionMap이 생성될때 단 한번만 멤버 함수 포인터를 입력시키는 방법이다. 그건 쉽게 만들수 있는데, 우리 단지 자역 정적 함수로서 inializeCollisionMap을 호출해서 map의 초기화와 생성을 담당하게 만들면 된다. inializeCollisionMap의 반환 인자로 collisionMap을 초기화 한다.

~cpp 
class SpaceShip: public GameObject {
private:
    static HitMap initializeCollisionMap();
    ...

};

SpaceShip::HitFunctionPtr
SpaceShip::lookup(const GameObject& whatWeHit)
{
    static HitMap collisionMap = initializeCollisionMap();
    ...
}
하지만 이것은 initializeCollisionMap이 반환하는 map객체를 collsionMap에 복사하면서 비용을 지출한다. (Item 19, 20 참고) 이를 행하지 않게 만들어야 한다. 만약 inializeCollisionMap이 포인터를 반환하면 비용 지출을 하지 않지만, map객체가 파괴되어야 하는 것에 혼란을 가지고 올수 있다.

다행히도, 여기에는 방법이 있다. 우린 collsionMap을 스마트 포인터(Item 28참고)로 자동으로 삭제되도록 만들어 버리는 것이다. 사실 C++ 표준 라이브러리에 있는 템플릿인, auto_ptr는 이런 상황에 적합할 것이다. (Item 9참고) collsionMap을 정적 auto_ptr로 lookup내부에서 만드는 것으로 initializeCollisionMap이 반환하는 초기화된 map 객체를 건네 받을수 있다. 이렇게 하면 자원이 세는것을 걱정할 필요는 아직 없다. collisionMap이 가리키는 map객체는 자동으로 삭제될것이다. 그래서 :

~cpp 
class SpaceShip: public GameObject {
private:
    static HitMap * initializeCollisionMap();
    ...
};

SpaceShip::HitFunctionPtr
SpaceShip::lookup(const GameObject& whatWeHit)
{
    static auto_ptr<HitMap>  collisionMap(initializeCollisionMap());
    ...
}
initializeCollisionMap의 구현을 명확히 한다.

~cpp 
SpaceShip::HitMap * SpaceShip::initializeCollisionMap()
{
    HitMap *phm = new HitMap;

    (*phm)["SpaceShip"] = &hitSpaceShip;
    (*phm)["SpaceStation"] = &hitSpaceStation;
    (*phm)["Asteroid"] = &hitAsteroid;

    return phm;
}
그렇지만 앞에서 말한것과 같이 이건 컴파일 되지 않는다. 이유는 HitMapGameObject 이름 지어진 클래스의 형을 인자로 하는 멤버 함수의 포인터를 잡을수 있도록 만들었기 때문이다. 그렇지만 hitSpaceShip은 SpaceShip을 인자로 하고 hitSpaceStation은 SpaceStation그리고 hitAsteroid는 Asteroid를 인자로 한다. SpaceShipSpaceStation과 Asteroid가 암시적(implicit)으로 GameObject로 형변환 할수 있더라도, 위의 표현시에 그러한 형변환은 없다.

컴파일러에게 이를 받아들이게 하려면 reinterpret_cast(Item 2참고)를 임시적으로 써서, 함수 포인터 형간의 형변환시에 형변환을 써야 한다.

~cpp 
// 않좋은 생각
SpaceShip::HitMap * SpaceShip::initializeCollisionMap()
{
    HitMap *phm = new HitMap;

    (*phm)["SpaceShip"] = reinterpret_cast<HitFunctionPtr>(&hitSpaceShip);

    (*phm)["SpaceStation"] = reinterpret_cast<HitFunctionPtr>(&hitSpaceStation);

    (*phm)["Asteroid"] = reinterpret_cast<HitFunctionPtr>(&hitAsteroid);

    return phm;
}
이는 컴파일이 될것이다. 하지만 좋지 않은 생각이다. 이는 결코 원하지 않은 효과까지 동반한다.:컴파일러에게 거짓말을 하는것. hitSpace, hitSpaceStation, hitAsteroid 이 GameObject를 수용할수 있을것 처럼 하는것은 거짓이다. hitSpaceShip는 Space, hitSpaceStation은 SpaceStation, hitAsteroid는 Asteroid를 수용한다. 하지만 위와 같은 것은 거짓말을 하는 것이다.

좀더 인간적인 시각에서 본다. 컴파일러는 거짓말을 좋와하지 않는다. 그들은 원하는 것에 대한 정확한 방법을 찾는다. 이런 경우에, 컴파일러는 GameObject에서 다중 상속 관계를 가지고 있거나, 가상 기초 클래스의 관계를 가지고 있는 경우 *phm을 통하여 호출하는 함수에 대하여 잘못된 코드가 작성된다. 다름 말로 하자면, 만약 SpaceStation, SpaceShip, Asteroid가 다른 클래스(덫붙여 GameObject)를 가지오 있다면 당신은 아마 collide에서 충돌 수행함수의 호출하는 것이 상당히 어렵다는걸 알수 있다.

A-B-C-D의 상속 관계가 Item 24처럼 이렇게 되어 있다고 해보자.


객체 D안에는 각 4개의 클래스 영역이 다른 주소를 가지고 있다. 이는 중요하다. 왜냐하면, 포인터와 참조가 각기 다르게 작용하지만, 컴파일러는 보통 생성된 코드 내에서 포인터를 이용해서 참조를 구현한다. 그래서 pass-by-reference 는 보통 객체를 가리키는 포인터로 구현된다. 다중 상속을 한 객체가(가령 D객체) 참조로 넘기면(pass-by-reference) 해당 형의 함수를 부르기위해 정확한 주소가 필요한 컴파일러에게 피해를 준다.

그러나, 당신이 컴파일러에게 거짓을 말해 놓을때 즉, 실제로는, SpaceShip이나 SpaceStation 예상할때 GameObject로 예상되는 함수를 언급해 놓으면 어떨까? 그렇게 되면 컴파일러는 잘못된 주소를 함수 호출시에 넘기고, 결과적으로 실행 시간에서 프로그램은 아수라장이 되어 버린다. 또한 매우 어려운 문제가 발생되는 것이다. 이는 형변환의 단점을 증명하는 좋은 예이다.

좋다. 그래서 형변환은 안된다. 그래. 하지만 HitMap이 가지고자 하는 hitSpaceShip, hitSpaceStation, hitAsteroid 함수를 수행할 함수 포인터들 사이에 형이 안맞는다. 여기에는 강제적인 해결 책밖에는 없다.:함수의 형을 바꾸어주어서 GameObject를 수용하게 해버리는것

~cpp 
class GameObject {                    // this is unchanged
public:
    virtual void collide(GameObject& otherObject) = 0;
    ...
};

class SpaceShip: public GameObject {
public:
    virtual void collide(GameObject& otherObject);

    // 함수의 모든 인자를 GameObject 로 바꾸었다.
    virtual void hitSpaceShip(GameObject& spaceShip);
    virtual void hitSpaceStation(GameObject& spaceStation);
    virtual void hitAsteroid(GameObject& asteroid);
  ...
};
double-dispatch 문제의 해결책은 collide를 이름으로 함수를 오보로드(overload)한 가상 함수를 기반으로 하고 있다. 이제 우리는 왜 짝짓는 것이 안되는지 이해를 한다.-관련배열(associative array)에 멤버 함수 포인터 사용의 결정 이유. 모든 hit함수는 같은 인자를 받는다. 그래서 단지 다른 이름만을 우리는 준다.

이제, 이 initializeCollisionMap을 작성할수 있다.

~cpp 
SpaceShip::HitMap * SpaceShip::initializeCollisionMap()
{
    HitMap *phm = new HitMap;

    (*phm)["SpaceShip"] = &hitSpaceShip;
    (*phm)["SpaceStation"] = &hitSpaceStation;
    (*phm)["Asteroid"] = &hitAsteroid;

    return phm;
}
유감스럽게, 이제 우리의 hit함수는 유도된 함수를 인자로 하는 대신에, GameObject를 인자로 가진다. 이제 내부에서 인자를 쓰기위해서는 반드시 예상되는 인자로 dynamic_cast를 해주어야 한다.(Item 2참고) 각 함수는 다음과 같은 구조이다.

~cpp 
void SpaceShip::hitSpaceShip(GameObject& spaceShip)
{
    SpaceShip& otherShip = dynamic_cast<SpaceShip&>(spaceShip);

    SpaceShip-SpaceShip 간의 충돌 수행;
}

void SpaceShip::hitSpaceStation(GameObject& spaceStation)
{
    SpaceStation& station = dynamic_cast<SpaceStation&>(spaceStation);

    SpaceShip-SpaceStation 간의 충돌 수행;
}

void SpaceShip::hitAsteroid(GameObject& asteroid)
{
    Asteroid& theAsteroid =  dynamic_cast<Asteroid&>(asteroid);

    SpaceShip-Asteroid 간의 충돌 수행;
}
형변환이 실패하면, 각 dynamic_cast가 bad_cast 예외를 던질 것이다. 물론 정확히 불린다면 그들은 결코 실패하지 않는다.

1.5. Using Non-Member Collision-Processing Functions :비멤버 함수들로 충돌 처리

우린 double-dispatch에서 vtbl과 같은 관련배열(associative array)만드는 방법을 알았다. 그리고 lookup함수 내부에서 관련배열의 캡슐화의 구체적 부분에 관해서 안다. 하지만 이러한 배열들이 멤버 함수 포인터를 가지기 때문에 우린 아직 GameObject 타입의 새로운 형태가 게임에 추가되면 클래스를 수정해야만 한다. 예를 들어서 다시 Satellite가 게임에 추가되면, 우리는 SpaceShip 클래스와의 관계를 고려하여, spaceship과 satellite간의 충돌을 잡는 구문을 추가해야 한다. 이경우, Satellite와 관계 없는 모든 SpaceShip 클라이언트도 재컴파일 되어야 한다. 이 문제는 순수하게 가상 함수 기반의 double-dispat 구현에서도 제기되었고, 해결책은 좀 일(코딩)을 적개 하는 방향으로 만들어 나가는것 뿐이었다.

재컴파일 문제는 만약 우리가 비멤버 함수들로 관련 배열을 구성하면 해결할수 있다. 게다가 비맴버 충돌 처리 함수는 아마 디자인시에 우리가 지금까지의 의문을 무시하게 할수 있다. 자세히 마해 보자면 서로다른 형의 객체간의 충돌 처리는 어디에서 하는가? 우리의 구현에 의하면 object 1과 object 2가 충돌하면, object 1이 processCollision의 left-hand인자로 object 1의 내부에 충돌 처리 루틴에 검색된다. object 2 와 object 1(앞뒤만 바뀌었다.) 충돌하면 processCollision이 object 2에 대한 클래스의 내부 함수가 호출된다. 이게 좋와 보이는가? 객체 A,B에 대한 충돌 처리는 A,B어디에 잡히든 외부에서 두 객체를 처리하는 디자인보다 좋와보이지 않지 않은가?

만약 클래스의 바깥으로 충돌 처리 과정을 이동 시킨다면, 어떠한 hit나 collide를 제한 클래스 정의를 담은 해더파일을 클라이언트에게 공급할수 있다. 그래서 이런 생각으로 구현한 processCollision이 다음과 같다.
(작성자주:앗싸! 소스 코드 왕창이다.)

~cpp 
#include "SpaceShip.h"
#include "SpaceStation.h"
#include "Asteroid.h"

namespace {     // 이름 없는 namespace(이름 공간)? 아래 설명을 보자.

    // 충돌 처리 함수 첫번째
    void shipAsteroid(GameObject& spaceShip, GameObject& asteroid);

    void shipStation(GameObject& spaceShip, GameObject& spaceStation);

    void asteroidStation(GameObject& asteroid, GameObject& spaceStation);
    ...

    // 두번째 충돌 처리 함수들, 첫번째 인자를 바꾸어서 delegate 하는 것이다.
    void asteroidShip(GameObject& asteroid, GameObject& spaceShip)
    { shipAsteroid(spaceShip, asteroid); }

    void stationShip(GameObject& spaceStation, GameObject& spaceShip)
    { shipStation(spaceShip, spaceStation); }

    void stationAsteroid(GameObject& spaceStation, GameObject& asteroid)
    { asteroidStation(asteroid, spaceStation); }

    ...

    // 형/함수 로 관련 배열 만든느 과정, 설명 부분 참고
    typedef void (*HitFunctionPtr)(GameObject&, GameObject&);
    typedef map< pair<string,string>, HitFunctionPtr > HitMap;

    pair<string,string> makeStringPair(const char *s1, const char *s2);

    HitMap * initializeCollisionMap();

    HitFunctionPtr lookup(const string& class1, const string& class2);

} // namespace 끝

void processCollision(GameObject& object1, GameObject& object2)
{
  HitFunctionPtr phf = lookup(typeid(object1).name(),typeid(object2).name());

  if (phf) phf(object1, object2);
  else throw UnknownCollision(object1, object2);
}
processCollision 구현 부분이 이름 없는 namespace(unnamed namespace)으로 사용 되었음을 주목하라. 그런 이름없는 namespace 내의 모든 것은 현재 해석 단위(translation unit:현재 파일)에서 사용(private)이다.-파일의 영역에서 staitc으로 선언된 함수들과 같이 동작한다. 그렇지만 namespace의 등장으로 파일 영역에서 static 인자들은 별로 좋지 못하다. 그래서 당신의 컴파일러가 이름 없는 namespace를 지원하면 그걸 사용하도록 습관 들여야 할것이다.

개념적으로, 멤버 함수를 이용해서 구현하는 것과 동일한 효과를 주지만, 약간 다른점이 있다. 첫번째HitFunctionPtr은 비멤버 함수 형의 포인터이다. 두번째CollisionWithUnknownObject 예외는 UnknownCollision로 이름이 바뀌었고, 하나가 아닌, 두개의 객체를 받도록 수정되었다. 마지막으로 lookup은 반드시 양쪽의 double-dispatch를 위해 두개의 인자를 받아들인다. 이것은 충돌 체크를 위한 map이 세개의 인자에 대한 정보를 가지는 것을 의미한다.:두가지의 형과, 하나의 HitFunctionPtr정보

표준 map클래스는 오직 두가지의 정보만을 가지고 있을수 있는것이 법칙이다. 우린 두가지 형의 이름을 하나로 묶기위해, 표준 pair 템플릿을 이용해서 이 문제를 해결해 나간다. initializeCollisionMap, 은 다음과 같이 makeStringpair 도움 함수를 이용한다. 코드는 다음과 같다.

~cpp 
// 두가지의 char* 리터럴로 pair<string,string> 객체를 생성한다.
// 이는 initializeCollisionMap에서 사용된다. 반환 인자의 최적화는 Item 20을 참고하라.
namespace {     // 이름 없는 namespace(이름 공간) 다시? 글에 설명 참고
  pair<string,string> makeStringPair(const char *s1, const char *s2)
  { return pair<string,string>(s1, s2);   }
} // namespace 끝

namespace {     // 아직도 이름없는 namespace(이름공간) ? 글에 설명 참고
    HitMap * initializeCollisionMap()
    {
        HitMap *phm = new HitMap;

        (*phm)[makeStringPair("SpaceShip","Asteroid")] = &shipAsteroid;

        (*phm)[makeStringPair("SpaceShip", "SpaceStation")] = &shipStation;
        ...
        return phm;
    }
} // end namespace

lookup역시 pair<string,string> 객체를 이용해서 수정된다.

~cpp 
namespace {          // 밑에 설명한다 밑어 주세요.

    HitFunctionPtr lookup(const string& class1, const string& class2)
    {
        static auto_ptr<HitMap> collisionMap(initializeCollisionMap());

        // make_pair에 관해 글에 설명한다.
        HitMap::iterator mapEntry = collisionMap->find(make_pair(class1, class2));

        if (mapEntry == collisionMap->end()) return 0;

        return (*mapEntry).second;
    }
} // namespace 끝
우리가 전에 했던것과 거의 같다. 오직 다른점이라고는 make_pair함수 구문 바로 이것 이다.
{{[
HitMap::iterator mapEntry = collisionMap->find(make_pair(class1, class2));
}}}
make_pair은 단지 함수의 편의성을 위해서 사용되었고, 이것은 표준 라이브러리에서 pair객체가 만들어 질때 형을 만족시키는 문제를 해결한다. 다른 구문으로는 다음것이 있다.

~cpp 
HitMap::iterator mapEntry = collisionMap->find(pair<string,string>(class1, class2));
이 호출은 좀 더 형을 만족 시키도록 만들고, pair를 위해서 좀 더 알맞게 형을 맞추지만 이것은 헛수고이다.(class1과 class2의 형은 같다.), 그래서 make_pair의 형태는 좀더 일반적으로 사용된다.

makeStirngPair 때문에 initializeCollisionMap과 lookup은 이름없는 namespace에 선언된다. 둘은 같은 이름 공간에 선언되어져야만 한다. 그러한 이유로 위의 구현은 모두 이름없는(unamed) namespace에 구현되었다.(그들의 선언에서는 해석단위(파일)도 같아야 한다.):그래서 링커는 아마 그들의 미리 있는 선언과 더불어 정의(구현:implimentatation고 동일)를 정확히 관련 지을 것이다.

마지막으로 우리의 목표에 도달했다. 만약에 GameObject의 새로운 sub클래스가 우리의 계층에 추가된다면, 존재하는 클래스들에 대한 재 컴파일은 필요 없다.(그들이 새로운 클래스를 사용하기 전까지는) RTTI의 기반이나, 유지보수를 위한 if-then-else이 엉키지 않는다. 새로운 클래스가 계층에 더해지는 문제는 오직 우리의 시스템에 잘 정의된 변경에 기인한다.:initializeCollisionMap에 하나 이상의 map이 더해 지는 것, 그리고 processCollision의 구현과 관련된 이름없는 namespace에 새로운 충돌 처리 함수. 그것은 아마 좀더 많은 일을 가지고 올지도 모른다. 그렇지만 최소한 할만 한 것이다. 안그런가?

아마도... (Maybe..)

1.6. Inheritance and Emulated Virtual Function Tables : 상속과 가상함수 테이블 흉내

이제, 마지막 문제에 직면했다. (맨날 마지막이라고 해서 이상하게 여길지 모르는데, 이번에는 가상 함수의 구현을 위한 디자인상의 어려움에 진짜 직면했다.) 충돌 처리 과정의 함수들을 호출할때, 지금까지 해온 모든 것 들은 상속을 하지 않을 경우 재대로 작동한다. 그렇지만, 우리가 개발하는 게임에서 군용 우주 비행선(military space ship)과, 상업용 우주 비행선(commercial space ship) 구분이 필요하다고 해보자. 그럼 이제 보이는데로, 상속 관계에 대하여 수정을 할수 있다. Item 33의 내용을 보고 유의해야 하고, CommercialShipMilitaryShip이 새로운 가상 클래스인 SpaceShip으로 상속한 완전한 클래스로 만들어야 하는 내용이 아래에 있다.


상업용, 군용 ship이 어떤것과 충돌할때 동일한 행동을 한다고 가정하자. 그리고 나면, CommercialShipMilitaryShip이 추가되기 전과, 동일한 충돌 처리를 할수 있다. 특별히 만약 MilitaryShip 객체와 Asteroid가 충돌할때를 예상해 보면

~cpp 
void shipAsteroid(GameObject& spaceShip, GameObject& asteroid);
이렇게 호출될 것이다. 하지만 할수가 없다. 대신에 UnknownCollision 예외가 발생한다. 왜냐하면 lookup은 아마도 형에 맞는 함수를 찾을때, 형의 이름음 "MilitaryShip"과 "Asteroid"를 찾고, 그러한 함수가 collisionMap상에 존재하지 않기 때문이다. MilitaryShipSpaceShip과 같은 취급을 받을수 있지만, lookup은 이를 알 방법이 없다.

게다가, 구현 과정도 쉽지 않다. 만약 당신이 중요한 double-dispatch이 필요하고, 이런 경우에, 상속 기반 인자들의 형변환이 가능하다면, 당신의 이런 특별난 형변환(방향 전환,recourse)은 double-virtual-function-call의 메커니즘에도 나왔듯이 실패할 것이다. 그것은 당신이 상속관계에서 추가할때마다 모든것을 재컴파일 해야 한다는 의미이기도 하다.

DeleteMe 모호 : but that's just the way life is sometimes.

1.7. Initializing Emulated Virtual Function Tables (Reprise) : 가상함수 테이블 흉내 (반복)

이제 double-dispatch에 관한 모든것을 이야기 했다. 그렇지만 결말이 좀 좋지 못하다. 대신에, collisionMap의 초기화에 대한 대안을 제시한다.

우리의 디자인은 전체적으로 정적이다.(static) 두가지 형태의 객체 사이의 충돌을 위한 처리 함수는 한번만 등록한다.;우리는 그 함수를 계속 유지한다.(DeleteMe 모호) 우리가 충돌 처리를 더하거나, 제거하고나, 변화시키려면 어떤가? 방법이 없다.

그렇지만 가능하다. 우리는 충돌 처리에 대한 함수를 관리하는 map의 개념을 돌아봐서, 수정가능하도록 map을 동적으로 바꾸어야 한다. 예를 들면:

~cpp 
class CollisionMap {
public:
    typedef void (*HitFunctionPtr)(GameObject&, GameObject&);

    void addEntry(const string& type1, const string& type2,
                HitFunctionPtr collisionFunction,
                bool symmetric = true);               // 아래 설명 참고

    void removeEntry(const string& type1, const string& type2);

    HitFunctionPtr lookup(const string& type1, const string& type2);

    // 이 함수는 map의 참조를 반환한다. Item 26참고
    static CollisionMap& theCollisionMap();

private:
    // 사역으로 묶는 것은 map을 여러게 만드는걸 방지 (Item 26참고)
    CollisionMap();
    CollisionMap(const CollisionMap&);
};
이 클래스는 map에 엔트리(entry:컴용어로, 데이터의 인자)를 추가하거나 제거할수 있고, 특정한 형의 이름과 짝을 이루어서 충돌 처리 함수를 찾을수 있다. 또한 Item 26에서의 기술을 사용해서 CollisionMap 객체의 숫자 역시 하나로 제한한다. 왜냐하면, 시스템에서 map은 하나만 존재하기 때문이다.( 더 많은 map의 지원하면, 더 복잡한 게임의 구현이 쉬울것이다. 마지막으로 map에 대칭적인 충돌 처리 루틴을 간단히 추가를(다시 말해 type T1과 type T2의 충돌시 처리와, type T2와 type T1의 충돌 처리) 자동으로 addEntry의 옵션중 symmetric을 true로 세팅해서 수행한다.

CollisionMap클래스를 이용해서, 각 클라이언트는 map의 엔트리에 직접 접근해 추가한다.

~cpp 
void shipAsteroid(GameObject& spaceShip,
                  GameObject& asteroid);
CollisionMap::theCollisionMap().addEntry("SpaceShip",
                                         "Asteroid",
                                         &shipAsteroid);

void shipStation(GameObject& spaceShip,
                 GameObject& spaceStation);
CollisionMap::theCollisionMap().addEntry("SpaceShip",
                                         "SpaceStation",
                                         &shipStation);

void asteroidStation(GameObject& asteroid,
                     GameObject& spaceStation);
CollisionMap::theCollisionMap().addEntry("Asteroid",
                                         "SpaceStation",
                                         &asteroidStation);

이 map 엔트리에 함수의 추가가, 해당 함수에 관련된 어떠한 충돌 처리가 일어나는것 보다 먼저 일어나야 함을 유의해라. GameObject의 서브 클래스의 생성자에서 객체가 생성될때, 각 알맞는 map 엔트리가 있나 확인 해야 한다. 그러한 접근은 실행 시간에 성능에 작은 단점으로 작용한다. 대안으로는 RegisterCollisionFunction클래스를 생성하는 것이다.

~cpp 
class RegisterCollisionFunction {
public:
    RegisterCollisionFunction(
                const string& type1,
                const string& type2,
                CollisionMap::HitFunctionPtr collisionFunction,
                bool symmetric = true)
    {
        CollisionMap::theCollisionMap().addEntry(type1, type2,
                                                collisionFunction,
                                                symmetric);
    }
};
클라이언트들은 전역 객체로서 이 형을 사용하고 자동으로 추가되도록 한다.

~cpp 
RegisterCollisionFunction cf1("SpaceShip", "Asteroid",
                              &shipAsteroid);

RegisterCollisionFunction cf2("SpaceShip", "SpaceStation",
                              &shipStation);

RegisterCollisionFunction cf3("Asteroid", "SpaceStation",
                              &asteroidStation);
...
int main(int argc, char * argv[])
{
    ...
}
이 객체는 main이 호출되기 이전에 불려야 하기 때문에, 그들의 생성자 들은 main이전에 호출되면서 등록된다. 만약 늦을 경우 새로운 클래스가 등록되면

~cpp 
class Satellite: public GameObject { ... };
그리고 하나 이상의 새로운 충돌 처리 함수가 작성된다면

~cpp 
void satelliteShip(GameObject& satellite,
                   GameObject& spaceShip);

void satelliteAsteroid(GameObject& satellite,
                       GameObject& asteroid);
이러한 새로운 함수는 존재하는 코드에 제한 없이 map에 간단히 추가된다.

~cpp 
RegisterCollisionFunction cf4("Satellite", "SpaceShip",
                              &satelliteShip);

RegisterCollisionFunction cf5("Satellite", "Asteroid",
                              &satelliteAsteroid);

multiple dispatch 구현에 최적의 방법이 없다는 것에는 변함이 없다. 하지만 map기반으로 우리가 의도하고자 하는 접근에 더 좋은 방법을 제시할 것이다.

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