2017-09-05 21 views
3

동일한 가상 함수를 공유하는 객체 그룹을 보유하기 위해 기본 클래스에 대한 포인터 컨테이너를 사용하는 사용자를 보았습니다. 이러한 기본 클래스 포인터를 사용하여 파생 클래스의 오버로드 된 함수를 사용할 수 있습니까? 무슨 뜻인지 설명하기 어렵다하지만 (나는 생각한다) 쉽게 내 첫번째 생각은 이러한 기능이 있습니다 (아래 그림 참조) 가상 클래스의 구성원이 될 수 있도록했다C++의 기본 클래스에 대한 포인터 만있는 경우 파생 클래스 인수로 함수 오버로드

class PhysicsObject // A pure virtual class 
{ 
    // Members of physics object 
    // ... 
}; 

class Circle : public PhysicsObject 
{ 
    // Members of circle 
    // ... 
}; 

class Box : public PhysicsObject 
{ 
    // Members of box 
    // ... 
}; 

// Overloaded functions (Defined elsewhere) 
void ResolveCollision(Circle& a, Box& b); 
void ResolveCollision(Circle& a, Circle& b); 
void ResolveCollision(Box& a, Box& b); 

int main() 
{ 
    // Container to hold all our objects 
    std::vector<PhysicsObject*> objects; 

    // Create some circles and boxes and add to objects container 
    // ... 

    // Resolve any collisions between colliding objects 
    for (auto& objA : objects) 
     for (auto& objB : objects) 
      if (objA != objB) 
       ResolveCollision(*objA, *objB); // !!! Error !!! Can't resolve overloaded function 
} 

코드를 보여하지만 난 빨리 정확하게 가지고 실현 같은 문제. 그것은 가능성이 주조를 사용하여 해결할 수 있습니다하지만 난에 캐스팅 올바른 유형을 찾는 방법을 알아낼 수 없습니다 (또한 추한)처럼 보인다 문제를 인터넷 검색에서

class Circle; 
class Box; 
class PhysicsObject // A pure virtual class 
{ 
    virtual void ResolveCollision(Circle& a) = 0; 
    virtual void ResolveCollision(Box& a) = 0; 
    // Members of physics object 
    // ... 
}; 

class Box; 
class Circle : public PhysicsObject 
{ 
    void ResolveCollision(Circle& a); 
    void ResolveCollision(Box& a); 
    // Members of circle 
    // ... 
}; 

class Circle; 
class Box : public PhysicsObject 
{ 
    void ResolveCollision(Circle& a); 
    void ResolveCollision(Box& a); 
    // Members of box 
    // ... 
}; 

. 나는 잘못된 질문을하고 있는데이 문제를 회피하고 동일한 결과를 얻는 코드를 구조화하는 더 좋은 방법이 있다고 생각한다. 이중 파견으로

+3

[double dispatch] (https://en.wikipedia.org/wiki/Double_dispatch)를 참조하십시오. – Jarod42

+0

문제는 명백합니다. 과부하는 2 개의 PhysicsObject를 인수로 사용하므로 오류가 발생합니다. 오버로드를 제공하거나'dynamic_cast'를 사용하십시오. –

+0

물론 두 개의 PhysicsObjects를 사용하는 과부하를 생성 할 수는 있지만 여전히 동일한 유형의 물리 개체가 어떤 종류인지 알지 못하기 때문에 동일한 문제가 발생합니다. 감사합니다 @ Jarod42, 지금 읽어 줄 것입니다. – Beetroot

답변

2

, 그것은 같은 것 :

class Circle; 
class Box; 

// Overloaded functions (Defined elsewhere) 
void ResolveCollision(Circle& a, Box& b); 
void ResolveCollision(Circle& a, Circle& b); 
void ResolveCollision(Box& a, Box& b); 
class PhysicsObject // A pure virtual class 
{ 
public: 
    virtual ~PhysicsObject() = default; 

    virtual void ResolveCollision(PhysicsObject&) = 0; 
    virtual void ResolveBoxCollision(Box&) = 0; 
    virtual void ResolveCircleCollision(Circle&) = 0; 
}; 

class Circle : public PhysicsObject 
{ 
public: 
    void ResolveCollision(PhysicsObject& other) override { return other.ResolveCircleCollision(*this); } 
    void ResolveBoxCollision(Box& box) override { ::ResolveCollision(*this, box);} 
    void ResolveCircleCollision(Circle& circle) override { ::ResolveCollision(*this, circle);} 
    // ... 
}; 

class Box : public PhysicsObject 
{ 
public: 
    void ResolveCollision(PhysicsObject& other) override { return other.ResolveBoxCollision(*this); } 
    void ResolveBoxCollision(Box& box) override { ::ResolveCollision(box, *this);} 
    void ResolveCircleCollision(Circle& circle) override { ::ResolveCollision(circle, *this);} 
    // ... 
}; 
+0

당신의 코드는 그것을 분명하게 보여 주지만 그것을 언급하기 만하면됩니다. 명백한 단점은'n' 관련 클래스의 경우 모든 단일 클래스가'n' 메소드를 필요로한다는 것입니다. –

+0

감사합니다. 전에 이중 디스패치에 대해 들어 보지 못했지만 대답을 사용하면 내 코드에 쉽게 적응할 수있었습니다. 너가 너무 많은 모양을 가지지 않는다면 그것은 간단한 해결책처럼 보인다. – Beetroot

+0

[Multiple dispatch] (https://stackoverflow.com/a/29345504/2684539)도 했으므로 사용이 간단해질 수 있습니다 (전체 구현이 더 복잡해도). – Jarod42

2

나는 이것이 아마에 대한 객체의 물리적 경계에 대해 알려주는 Extent 클래스를 구축하는 것입니다 할 줄 방법을 그 중부 표준시. 또한, 당신은 PhysicsObject 클래스

virtual const Extent& getExtent() const = 0;

이있을 것이다. 그런 다음 객체 유형 당 한 번 getExtent을 구현합니다.

귀하의 충돌 감지 라인은

ResolveCollision(objA->getExtent(), objB->getExtent()); 
의미에서, 이것은 당신 때문에 접근이 잘 확장 할 수 복잡성이 Extent 클래스에 밀려으로 도로 아래로 깡통을 걷어차보다 조금 더 않습니다,하지만

된다 객체 당 하나의 메소드 만 작성하면됩니다.

대체 이중 디스패치 메커니즘은 새로운 모양이 모두 기존 모양으로 조정되어야하기 때문에 방해가됩니다. Circle 클래스를 다시 컴파일해야하는 경우, 예를 들어, Ellipse 클래스를 도입하면 말하자면 코드 냄새입니다.

+0

이것은 가장 쉬운 방법 일 수 있습니다. 그러나 'Extent' 클래스는 모양이 다양하고 매우 다양 할 경우 복잡하거나 복잡해질 수 있습니다. 당신이 마주 칠지도 모르는 또 다른 이슈는 이제는 더 이상 실제 객체가 아닌 범위에서 작업하고 있다는 것입니다. 충돌 만 감지하면 충분합니다. 탐지 된 충돌에 따라 개체에 힘을가하거나 충돌시 호출되는 콜백에 개체를 전달해야하는 경우 실제 모양의 유형 (및 포인터)이 필요할 수 있습니다. –

+0

@nh_하지만 영리한 사람이라면 Extent 클래스가 렌더링을 대신 할 수도 있습니다. 그리고 관성의 순간과 같은 것들은 둘레와 중심점의 함수 일뿐입니다 (균일 한 밀도를 가정 할 때), 그래서 그것은 또한 회전을 할 수 있습니다. – Bathsheba

+0

의심의 여지가 없습니다. 'Extent'를 구현하는 방법에 대해서 궁금합니다. 중심 + 지름과 상자 지경을 구별하지 않아도 될까요? 그리고 이것은 (a-> isCircular() && b-> isCircular()) {/*...*/} else if (a-> isCircular() && b-> isBox()) {/*...*/} else ...'? –

2

이중 디스패치에 의존하지 않는 구현을 스케치 할 것입니다. 대신 모든 기능이 등록 된 테이블을 사용합니다. 이 테이블은 객체의 동적 유형 (기본 클래스로 전달됨)을 사용하여 액세스됩니다.

먼저, 몇 가지 예시 모양이 있습니다. 그들의 유형은 enum class 안에 들어 있습니다. 모든 도형 클래스는 해당 열거 형 항목으로 MY_TYPE을 정의합니다. 또한, 그들은 기본 클래스 '순수 가상 type 방법을 구현해야한다 :

enum class ObjectType 
{ 
    Circle, 
    Box, 
    _Count, 
}; 

class PhysicsObject 
{ 
public: 
    virtual ObjectType type() const = 0; 
}; 

class Circle : public PhysicsObject 
{ 
public: 
    static const ObjectType MY_TYPE = ObjectType::Circle; 

    ObjectType type() const override { return MY_TYPE; } 
}; 

class Box : public PhysicsObject 
{ 
public: 
    static const ObjectType MY_TYPE = ObjectType::Box; 

    ObjectType type() const override { return MY_TYPE; } 
}; 

다음, 당신은 그들이 물론, 모양에 따라 구현 될 필요가, 당신의 충돌 해결 기능을 가지고 있습니다.

void ResolveCircleCircle(Circle* c1, Circle* c2) 
{ 
    std::cout << "Circle-Circle" << std::endl; 
} 

void ResolveCircleBox(Circle* c, Box* b) 
{ 
    std::cout << "Circle-Box" << std::endl; 
} 

void ResolveBoxBox(Box* b1, Box* b2) 
{ 
    std::cout << "Box-Box" << std::endl; 
} 

주, 우리는 Circle 가지고 - 나는 그들의 충돌이 같은 방법으로 감지 가정으로 Circle, - 여기 Box, 아니 Box을.Box에 대한 자세한 내용 - Circle 충돌 사례. 코어 부에 지금

함수 테이블 :

std::function<void(PhysicsObject*,PhysicsObject*)> 
    ResolveFunctionTable[(int)(ObjectType::_Count)][(int)(ObjectType::_Count)]; 
REGISTER_RESOLVE_FUNCTION(Circle, Circle, &ResolveCircleCircle); 
REGISTER_RESOLVE_FUNCTION(Circle, Box, &ResolveCircleBox); 
REGISTER_RESOLVE_FUNCTION(Box, Box, &ResolveBoxBox); 

테이블 자체는 std::function (S)의 2 차원 어레이이다. 이러한 함수는 파생 클래스가 아닌 PhysicsObject에 대한 포인터를 허용한다는 점에 유의하십시오. 그런 다음 등록을 쉽게하기 위해 일부 매크로를 사용합니다. 물론 각각의 코드는 손으로 작성 될 수 있으며 매크로 사용은 일반적으로 나쁜 습관으로 간주된다는 사실을 알고 있습니다. 그러나, 내 의견으로는, 이러한 종류의 것들은 매크로가 좋은 것이고, 네임 스페이스를 혼란스럽게하지 않는 의미있는 이름을 사용하는 한 허용됩니다. 다시 Circle - Box 만 등록되고 다른 방법은 등록되지 않습니다. 이제 환상 매크로

:

#define CONCAT2(x,y) x##y 
#define CONCAT(x,y) CONCAT2(x,y) 

#define REGISTER_RESOLVE_FUNCTION(o1,o2,fn) \ 
    const bool CONCAT(__reg_, __LINE__) = []() { \ 
     int o1type = static_cast<int>(o1::MY_TYPE); \ 
     int o2type = static_cast<int>(o2::MY_TYPE); \ 
     assert(o1type <= o2type); \ 
     assert(!ResolveFunctionTable[o1type][o2type]); \ 
     ResolveFunctionTable[o1type][o2type] = \ 
      [](PhysicsObject* p1, PhysicsObject* p2) { \ 
        (*fn)(static_cast<o1*>(p1), static_cast<o2*>(p2)); \ 
      }; \ 
     return true; \ 
    }(); 

매크로 고유 한 이름 변수 (행 번호를 사용하여)을 정의하지만,이 변수는 단순히 초기화 람다 함수가 실행되도록 내부 코드를 생성하는 역할을한다. 전달 된 두 개의 인수 (이들은 BoxCircle이라는 구체적인 클래스)의 유형 (ObjectType 열거 형)을 가져 와서 테이블을 인덱싱하는 데 사용됩니다. 전체 메커니즘은 유형 (열거 형에 정의 된대로)에 대해 전체 순서가 있다고 가정하고 Circle - Box 충돌에 대한 함수가 실제로이 순서로 인수에 등록되어 있는지 확인합니다. assert은 실수로 잘못하고 있는지 알려줍니다 (우연히 Box - Circle). 그런 다음이 특정 쌍의 유형에 대해 테이블 ​​내부에 람다 함수가 등록됩니다. 함수 자체는 유형이 PhysicsObject* 인 두 개의 인수를 취해 등록 된 함수를 호출하기 전에이를 구체적인 유형으로 변환합니다.

다음으로 테이블이 어떻게 사용되는지 살펴볼 수 있습니다.

void ResolveCollision(PhysicsObject* p1, PhysicsObject* p2) 
{ 
    int p1type = static_cast<int>(p1->type()); 
    int p2type = static_cast<int>(p2->type()); 
    if(p1type > p2type) { 
     std::swap(p1type, p2type); 
     std::swap(p1, p2); 
    } 
    assert(ResolveFunctionTable[p1type][p2type]); 
    ResolveFunctionTable[p1type][p2type](p1, p2); 
} 

, 인수의 동적 형태를 취하고 ResolveFunctionTable 내부 이러한 각 유형의 등록 기능에 전달 : 두 PhysicsObject (S)의 충돌을 확인하는 하나의 기능을 구현하기 위해 현재 쉽다. 명령이 순서에 맞지 않으면 인수가 스왑된다는 점에 유의하십시오. 따라서 ResolveCollisionBoxCircle과 함께 호출하면 Circle - Box 충돌로 등록 된 함수가 내부적으로 호출됩니다.

마지막으로, 나는 그것을 사용하는 방법의 예를 줄 것이다 :

int main(int argc, char* argv[]) 
{ 
    Box box; 
    Circle circle; 

    ResolveCollision(&box, &box); 
    ResolveCollision(&box, &circle); 
    ResolveCollision(&circle, &box); 
    ResolveCollision(&circle, &circle); 
} 

쉬운, 그렇지? 위의 작업 구현에 대해서는 this을 참조하십시오.


이제이 접근법의 장점은 무엇입니까? 위의 코드는 기본적으로 임의의 수의 도형을 지원하는 데 필요한 모든 것입니다. Triangle을 추가하려고한다고 가정 해 보겠습니다. 당신이 할 수있는 일은 다음과 같습니다

  1. ObjectType 열거에 항목 Triangle을 추가합니다.
  2. ResolveTriangleXXX 기능을 구현해야하지만 모든 경우에이 기능을 사용해야합니다.
  3. 하면 매크로를 사용하여 테이블에 등록 : 그것 뿐이다

합니다. PhysicsObject에 메소드를 추가 할 필요가 없으므로 모든 기존 유형에 메소드를 구현할 필요가 없습니다.

매크로를 사용하는 것과 같이 모든 접근법이 중앙 테이블 enum이고 글로벌 테이블에 의존하는 것과 같은이 접근법의 몇 가지 '결함'에 대해 알고 있습니다. 후자의 경우 셰이프 클래스가 여러 공유 라이브러리에 내장되어 있으면 문제가 발생할 수 있습니다. 그러나이 접근 방식은 다른 접근법 (예 : 이중 발송)의 경우처럼 코드가 폭발적으로 증가하지 않으므로 매우 유용합니다 (매우 특수한 경우는 제외).

+0

당신은 이것에 많은 노력을 기울였습니다. 비록 제가 할 수있는 방법은 아니지만 (제 접근법은 훨씬 단순합니다), 이것은 분명히 상향식의 가치가 있습니다. 그것은 그럴듯한 해결책입니다. 시간을내어 주셔서 감사합니다. – Bathsheba

+0

고맙습니다. 고맙습니다. –

+0

@nh_ 감사합니다. 솔루션에서 해결할 더 많은 모양을 추가 할 때 제대로 확장되지 않을 것이라는 점을 예견했습니다. 그들 모두가 다른 방식으로 도움을주기 때문에 대답을 선택하기가 어려웠습니다. 나는 당신이 아마도 많은 사람들에게 도움이 될 가장 일반적인 대답이었을 것이라고 생각하지만, 다른 대답은 개인적으로 지금 당장 구현하기가 더 쉬웠습니다. 나는 지금 당장 모든 개념에 익숙하지 않기 때문에 더 많은 시간을 할 때 다시 돌아와 답을 공부할 것입니다. – Beetroot