RTTI를 사용하는 것보다 '순수한 다형성'이 선호되는 이유는 무엇입니까?


106

이런 종류의 것을 논의하는 거의 모든 C ++ 리소스는 RTTI (런타임 유형 식별)를 사용하는 것보다 다형성 접근 방식을 선호해야한다고 알려줍니다. 일반적으로 저는 이런 종류의 조언을 진지하게 받아들이고 그 근거를 이해하려고 노력할 것입니다. 결국 C ++는 강력한 짐승이며 전체적으로 이해하기 어렵습니다. 그러나이 특정 질문에 대해서는 공백을 그리고 있으며 인터넷이 어떤 종류의 조언을 제공 할 수 있는지보고 싶습니다. 먼저 RTTI가 "유해한 것으로 간주되는"이유에 대해 인용 된 일반적인 이유를 나열하여 지금까지 배운 내용을 요약하겠습니다.

일부 컴파일러는 사용하지 않음 / RTTI가 항상 활성화되지는 않습니다.

나는 정말로이 논쟁을 사지 않는다. C ++ 14 기능을 지원하지 않는 컴파일러가 있기 때문에 C ++ 14 기능을 사용하지 말아야한다고 말하는 것과 같습니다. 그러나 아무도 C ++ 14 기능을 사용하는 것을 낙담하지 않을 것입니다. 대부분의 프로젝트는 사용중인 컴파일러와 구성 방법에 영향을줍니다. gcc 맨 페이지를 인용해도 :

-fno-rtti

C ++ 런타임 유형 식별 기능 (dynamic_cast 및 typeid)에서 사용할 가상 함수가있는 모든 클래스에 대한 정보 생성을 비활성화합니다. 언어의 해당 부분을 사용하지 않는 경우이 플래그를 사용하여 공간을 절약 할 수 있습니다. 예외 처리는 동일한 정보를 사용하지만 G ++는 필요에 따라이를 생성합니다. dynamic_cast 연산자는 런타임 유형 정보가 필요하지 않은 캐스트 (예 : "void *"또는 명확한 기본 클래스로 캐스트)에 계속 사용할 수 있습니다.

이것이 의미하는 바는 RTTI를 사용하지 않는 경우 비활성화 할 수 있다는 것입니다. 이는 Boost를 사용하지 않는 경우 링크 할 필요가 없다는 것과 같습니다. 누군가가 .NET으로 컴파일하는 경우를 계획 할 필요가 없습니다 -fno-rtti. 또한이 경우 컴파일러는 크고 명확하게 실패합니다.

추가 메모리 비용 / 느릴 수 있음

RTTI를 사용하고 싶을 때마다 일종의 유형 정보 나 클래스 특성에 액세스해야한다는 의미입니다. RTTI를 사용하지 않는 솔루션을 구현하는 경우 이는 일반적으로이 정보를 저장하기 위해 클래스에 일부 필드를 추가해야 함을 의미하므로 메모리 인수는 일종의 무효입니다 (이에 대한 예제를 더 아래에서 설명하겠습니다).

dynamic_cast는 실제로 느릴 수 있습니다. 그러나 일반적으로 속도가 중요한 상황에서 사용하지 않는 방법이 있습니다. 그리고 나는 대안을 잘 보지 못합니다. 이 SO 답변 은 기본 클래스에 정의 된 열거 형을 사용하여 유형을 저장하는 것을 제안합니다. 모든 파생 클래스를 미리 알고있는 경우에만 작동합니다. 그것은 상당히 큰 "if"입니다!

그 대답에서 RTTI의 비용도 명확하지 않은 것 같습니다. 다른 사람들은 다른 것을 측정합니다.

우아한 다형성 디자인으로 RTTI 불필요

이것이 제가 진지하게 받아들이는 조언입니다. 이 경우 RTTI 사용 사례를 다루는 좋은 비 RTTI 솔루션을 찾을 수 없습니다. 예를 들어 보겠습니다.

어떤 종류의 개체의 그래프를 처리하기 위해 라이브러리를 작성하고 있다고 가정 해 보겠습니다. 사용자가 내 라이브러리를 사용할 때 자신의 유형을 생성하도록 허용하고 싶습니다 (따라서 enum 메소드를 사용할 수 없음). 내 노드에 대한 기본 클래스가 있습니다.

class node_base
{
  public:
    node_base();
    virtual ~node_base();

    std::vector< std::shared_ptr<node_base> > get_adjacent_nodes();
};

이제 내 노드는 다른 유형이 될 수 있습니다. 이것들은 어떻습니까?

class red_node : virtual public node_base
{
  public:
    red_node();
    virtual ~red_node();

    void get_redness();
};

class yellow_node : virtual public node_base
{
  public:
    yellow_node();
    virtual ~yellow_node();

    void set_yellowness(int);
};

지옥, 왜 이것들 중 하나는 안될까요?

class orange_node : public red_node, public yellow_node
{
  public:
    orange_node();
    virtual ~orange_node();

    void poke();
    void poke_adjacent_oranges();
};

마지막 기능은 흥미 롭습니다. 작성하는 방법은 다음과 같습니다.

void orange_node::poke_adjacent_oranges()
{
    auto adj_nodes = get_adjacent_nodes();
    foreach(auto node, adj_nodes) {
        // In this case, typeid() and static_cast might be faster
        std::shared_ptr<orange_node> o_node = dynamic_cast<orange_node>(node);
        if (o_node) {
             o_node->poke();
        }
    }
}

이 모든 것이 명확하고 깨끗해 보입니다. 필요하지 않은 속성이나 메서드를 정의 할 필요가 없습니다. 기본 노드 클래스는 간결하고 의미를 유지할 수 있습니다. RTTI가 없으면 어디서부터 시작해야합니까? 기본 클래스에 node_type 속성을 추가 할 수 있습니다.

class node_base
{
  public:
    node_base();
    virtual ~node_base();

    std::vector< std::shared_ptr<node_base> > get_adjacent_nodes();

  private:
    std::string my_type;
};

std :: string 유형에 대한 좋은 아이디어입니까? 아닐 수도 있지만 다른 무엇을 사용할 수 있습니까? 번호를 만들어 다른 사람이 아직 사용하지 않기를 바랍니다. 또한 내 orange_node의 경우 red_node 및 yellow_node의 메소드를 사용하려면 어떻게해야합니까? 노드 당 여러 유형을 저장해야합니까? 복잡해 보입니다.

결론

이 예제는 지나치게 복잡하거나 비정상적으로 보이지 않습니다 (저는 일상 업무에서 유사한 작업을 수행하고 있습니다. 여기서 노드는 소프트웨어를 통해 제어되는 실제 하드웨어를 나타내며 무엇인지에 따라 매우 다른 작업을 수행합니다). 그러나 나는 템플릿이나 다른 방법으로 이것을 수행하는 깨끗한 방법을 알지 못할 것입니다. 나는 내 모범을 변호하는 것이 아니라 문제를 이해하려고 노력하고 있음을 유의하십시오. 위에서 링크 한 SO 답변과 Wikibooks의이 페이지 와 같은 페이지를 읽으면 RTTI를 오용하고 있음을 암시하는 것 같지만 그 이유를 알고 싶습니다.

그래서, 내 원래 질문으로 돌아가십시오. RTTI를 사용하는 것보다 '순수한 다형성'이 더 바람직한 이유는 무엇입니까?


9
poke oranges 예제를 해결하기 위해 "누락 된"(언어 기능으로서) 것은 다중 디스패치 ( "multimethods")입니다. 따라서이를 모방하는 방법을 찾는 것이 대안이 될 수 있습니다. 따라서 일반적으로 방문자 패턴이 사용됩니다.
다니엘 주르

1
문자열을 유형으로 사용하는 것은별로 도움이되지 않습니다. 일부 "유형"클래스의 인스턴스에 대한 포인터를 사용하면이 작업이 더 빨라집니다. 그러나 기본적으로 RTTI가 수행하는 작업을 수동으로 수행합니다.
다니엘 주르

4
@MargaretBloom 아니요, RTTI는 런타임 유형 정보를 의미 하는 반면 CRTP는 템플릿 전용입니다.
edmz

2
@ mbr0wn : 모든 엔지니어링 프로세스는 일부 규칙에 의해 구속됩니다. 프로그래밍도 예외는 아닙니다. 규칙은 소프트 규칙 (SHOULD)과 하드 규칙 (MUST)의 두 가지 버킷으로 나눌 수 있습니다 . (즉, 조언 / 옵션 버킷 (COULD)도 있습니다.) C / C ++ 표준 (또는 다른 영어 표준)이 어떻게 정의하는지 읽어보십시오. 당신의 문제는 당신이 "RTTI를 사용하지 않는다"를 어려운 규칙 ( "당신 은 RTTI를 사용하지 않는다")으로 착각했다는 사실에서 비롯된 것 같습니다. 그것은 당신이해야한다는 것을 의미 실제로 부드러운 규칙 ( "당신이 RTTI를 사용해서는 안 ')에 가능하면 그것을 피할 - 그리고 당신이 일을 피할 수없는 경우 만 사용하므로

3
많은 답변이 귀하의 예제 node_base가 라이브러리의 일부이고 사용자가 자신의 노드 유형을 만들 것이라고 제안한다는 생각을 주목하지 않습니다 . 그러면 다른 솔루션을 허용하도록 수정할 수 없으므로node_base RTTI가 최선의 선택이 될 수 있습니다. 다른 한편으로, RTTI (및 새로운 노드 유형을 설계하는 다른 방법)를 사용하지 않고도 새 노드 유형이 훨씬 더 우아하게 맞출 수 있도록 이러한 라이브러리를 설계하는 다른 방법이 있습니다.
마태 복음 월튼

답변:


69

인터페이스는 코드의 주어진 상황에서 상호 작용하기 위해 알아야 할 사항을 설명합니다. "전체 유형 계층"으로 인터페이스를 확장하면 인터페이스 "표면적"이 커져서 추론이 더 어려워집니다 .

예를 들어, "인접한 오렌지를 찌른다"는 것은 제 3 자로서 내가 오렌지를 모방 할 수 없다는 것을 의미합니다! 주황색 유형을 비공개로 선언 한 다음 RTTI를 사용하여 해당 유형과 상호 작용할 때 코드가 특별하게 작동하도록합니다. 내가 "주황색"이되고 싶다면, 나는 당신의 개인 정원에 있어야합니다 .

이제 "오렌지색"과 결합하는 모든 사람은 정의 된 인터페이스 대신 전체 주황색 유형과 암시 적으로 전체 개인 정원과 결합됩니다.

언뜻보기에 이것은 모든 클라이언트를 변경 (추가 am_I_orange) 하지 않고 제한된 인터페이스를 확장 할 수있는 좋은 방법처럼 보이지만 , 대신 발생하는 경향은 코드베이스를 골화시키고 추가 확장을 방지하는 것 입니다. 특별한 오렌지색은 시스템의 기능에 내재되어 있으며 다르게 구현되어 종속성을 제거하거나 다른 문제를 우아하게 해결할 수있는 오렌지 대신 "귤"대체물을 만들지 못하게합니다.

이것은 인터페이스가 문제를 해결하기에 충분해야 함을 의미합니다. 그런 관점에서 오렌지 만 찔러야하는 이유는 무엇입니까? 그렇다면 인터페이스에서 오렌지색을 사용할 수없는 이유는 무엇입니까? 임시로 추가 할 수있는 모호한 태그 집합이 필요한 경우 유형에 추가 할 수 있습니다.

class node_base {
  public:
    bool has_tag(tag_name);

이를 통해 좁은 범위에서 광범위한 태그 기반으로 인터페이스가 크게 확장됩니다. RTTI 및 구현 세부 정보 (일명 "어떻게 구현 되었습니까? 주황색 유형으로 통과 했습니까?")를 통해 수행하는 대신 완전히 다른 구현을 통해 쉽게 에뮬레이션되는 작업을 수행합니다.

필요한 경우 동적 메서드로 확장 할 수도 있습니다 . "당신은 Baz, Tom, Alice와의 논쟁으로 Foo'd가되는 것을지지합니까? Ok, Fooing you." 큰 의미에서 이것은 다른 객체가 여러분이 알고있는 유형이라는 사실을 파악하기 위해 동적 캐스트보다 방해가됩니다.

이제 tangerine 객체는 주황색 태그를 가질 수 있으며 구현이 분리되는 동안 함께 재생할 수 있습니다.

여전히 엄청난 혼란으로 이어질 수 있지만 구현 계층이 아니라 적어도 메시지와 데이터가 엉망입니다.

추상화는 관계를 분리하고 숨기는 게임입니다. 코드를 로컬에서 쉽게 추론 할 수 있습니다. RTTI는 구현 세부 사항에 대한 추상화를 통해 구멍을 뚫고 있습니다. 이렇게하면 문제를 더 쉽게 해결할 수 있지만 하나의 특정 구현에 매우 쉽게 고정되는 비용이 발생합니다.


14
마지막 단락에 +1; 내가 당신의 의견에 동의하기 때문이 아니라 여기에있는 망치질이기 때문입니다.

7
객체가 해당 기능을 지원하는 것으로 태그가 지정되었음을 알게되면 특정 기능을 어떻게 얻습니까? 이것은 캐스팅을 포함하거나 가능한 모든 멤버 함수를 가진 God 클래스가 있습니다. 첫 번째 가능성은 태그 지정이 자신의 매우 오류가있는 동적 유형 검사 체계 인 검사되지 않은 캐스팅이거나 dynamic_cast태그가 중복되는 경우 검사 (RTTI)입니다. 두 번째 가능성 인 신 클래스는 끔찍합니다. 요약하면이 답변에는 Java 프로그래머에게 좋게 들리는 단어가 많이 있지만 실제 내용은 의미가 없습니다.
건배와 hth. -Alf 2016 년

2
@Falco : 제가 언급 한 첫 번째 가능성 인 태그를 기반으로하여 확인되지 않은 캐스팅입니다. 여기서 태깅은 매우 취약하고 오류가 발생할 수있는 동적 유형 검사 체계입니다. 작은 클라이언트 코드 오작동 및 C ++에서는 UB-land에서 꺼져 있습니다. Java 에서처럼 예외가 발생하지 않지만 충돌 및 / 또는 잘못된 결과와 같은 정의되지 않은 동작이 발생합니다. 매우 불안정하고 위험 할뿐만 아니라 더 정상적인 C ++ 코드에 비해 매우 비효율적입니다. IOW., 매우 좋지 않습니다. 매우 그렇습니다.
건배와 hth. -Alf 2016 년

1
음. :) 인수 유형?
건배와 hth. -Alf

2
@JojOatXGME : "다형성"은 다양한 유형으로 작업 할 수 있음을 의미하기 때문입니다. 특정 유형 인지 확인해야한다면 포인터 / 참조를 시작하는 데 사용한 기존 유형 검사를 넘어서 다형성 뒤에있는 것입니다. 다양한 유형으로 작업하지 않습니다. 특정 유형으로 작업하고 있습니다. 예,이를 수행하는 "(대형) Java 프로젝트"가 있습니다. 하지만 그것은 자바입니다 . 언어는 동적 다형성 만 허용합니다. C ++에는 정적 다형성도 있습니다. 또한 "큰"사람이 그렇게한다고해서 좋은 생각이되지는 않습니다.
Nicol Bolas 2016 년

31

이것 또는 그 특징에 대한 도덕적 고소 의 대부분은 그 특징에 대한 오해 가 많다는 관찰에서 비롯된 입니다.

도덕 주의자들이 실패하는 곳 은 그들이 모든 용법이 오해되었다고 가정하는 것 입니다. 사실 특징은 이유가 있습니다.

그들은 내가 "배관 단지" 라고 부르던 것을 가지고 있습니다. 그들은 수리를 위해 호출 된 모든 수도꼭지 가 있기 때문에 모든 수도꼭지오작동 한다고 생각합니다 . 현실은 대부분의 탭이 잘 작동한다는 것입니다. 단순히 배관공을 부르지 않아도됩니다!

발생할 수있는 미친 일은 주어진 기능을 사용하지 않기 위해 프로그래머가 실제로 해당 기능을 정확히 다시 구현하는 상용구 코드를 많이 작성하는 경우입니다. (RTTI 나 가상 호출을 사용하지 않지만 실제 파생 된 유형을 추적 할 가치가있는 클래스를 만난 적이 있습니까? 이는 변장에서 RTTI의 재발 명에 지나지 않습니다 .)

다형성에 대해 생각하는 일반적인 방법이 있습니다 IF(selection) CALL(something) WITH(parameters). (미안하지만 추상화를 무시할 때는 프로그래밍이 전부입니다)

디자인 타임 (개념) 컴파일 타임 (템플릿 추론 기반), 런타임 (상속 및 가상 함수 기반) 또는 데이터 기반 (RTTI 및 스위칭) 다형성의 사용은 알려진 결정의 양에 따라 다릅니다. 생산 의 각 단계에서 어떻게 모든 상황에서 가변적 인지.

아이디어는 다음과 같습니다.

더 많이 예상할수록 오류를 포착하고 최종 사용자에게 영향을 미치는 버그를 피할 가능성이 높아집니다.

모든 것이 일정하다면 (데이터 포함) 템플릿 메타 프로그래밍으로 모든 것을 할 수 있습니다. 구현 된 상수에 대한 컴파일이 발생한 후 전체 프로그램결과 를 뱉어내는 return 문으로 요약됩니다. .

컴파일 타임에 모두 알려진 많은 경우가 있지만 실제 데이터에 대해 알지 못하는 경우 컴파일 타임 다형성 (주로 CRTP 또는 유사)이 해결책이 될 수 있습니다.

케이스의 선택이 데이터 (컴파일 시간 알려진 값이 아님)에 의존하고 전환이 1 차원 (하나의 값으로 만 축소 될 수 있음)이면 가상 함수 기반 디스패치 (또는 일반적으로 "함수 포인터 테이블") ")이 필요합니다.

전환이 다차원 인 경우 C ++에 네이티브 다중 런타임 디스패치 가 없기 때문에 다음 중 하나를 수행해야합니다.

  • Goedelization에 의해 1 차원으로 축소 : 다이아몬드누적 평행 사변형을 사용 하여 가상 기반과 다중 상속이 이루어지는 곳입니다. 이 이지만, 가능한 조합의 수를 알고 상대적으로 적어야합니다.
  • 차원을 서로 연결합니다 (복합 방문자 패턴에서와 같이, 모든 클래스가 다른 형제를 인식해야하므로 생각했던 위치에서 "확장"할 수 없음)
  • 여러 값을 기반으로 호출을 디스패치합니다. 이것이 바로 RTTI의 목적입니다.

전환뿐만 아니라 작업조차 컴파일 시간이 알려지지 않은 경우 스크립팅 및 구문 분석 이 필요합니다. 데이터 자체는 수행 할 작업을 설명해야합니다.

이제 제가 열거 한 각각의 경우가 그 뒤에 나오는 특정 사례로 볼 수 있기 때문에 최상위 문제에 대해 가장 낮은 솔루션을 남용하여 모든 문제를 해결할 수 있습니다.

그것이 도덕화가 실제로 피하려고하는 것입니다. 하지만 그렇다고 최하위 영역에 사는 문제가 존재하지 않는다는 의미는 아닙니다!

RTTI를 bashing하는 goto것은 bash를 bashing하는 것과 같습니다 . 프로그래머가 아닌 앵무새를위한 것.


각 접근 방식이 적용되는 수준에 대한 좋은 설명. 나는 "Goedelization"에 대해 들어 본 적이 없습니다. 다른 이름으로도 알려져 있습니까? 링크 나 설명을 추가해 주시겠습니까? 감사합니다 :)
j_random_hacker

1
@j_random_hacker : 저도 Godelization의 사용에 대해 궁금합니다. 일반적으로 Godelization은 문자열에서 정수로 매핑하는 첫 번째로 생각하고 두 번째는이 기술을 사용하여 형식 언어로 자기 참조 문을 생성하는 것입니다. 나는 가상 디스패치의 맥락에서이 용어에 익숙하지 않으며 더 배우고 싶습니다.
에릭 Lippert의

1
사실 저는 용어를 남용 하고 있습니다. Goedle에 따르면 모든 정수는 정수 n-ple (소수 인자의 거듭 제곱)에 해당하고 모든 n-ple은 정수에 해당하므로 모든 이산 n 차원 인덱싱 문제 는 다음과 같을 수 있습니다. 1 차원으로 축소됩니다 . 그것은 이것이 그것을하는 유일한 방법이라는 것을 의미하지 않는다. 그것은 단지 "가능하다"고 말하는 방법 일 뿐이다. 필요한 것은 "분할 및 정복"메커니즘입니다. 가상 기능은 "분할"이고 다중 상속은 "정복"입니다.
에밀리오 Garavaglia

... 유한 필드 (범위) 선형 조합 내에서 일어나는 모든 일 이 더 효과적 일 때 (고전적인 i = r * C + c는 행렬의 셀 배열에서 인덱스를 얻음). 이 경우 "방문자"와 정복자의 구분 ID는 "복합"입니다. 선형 대수가 포함되어 있기 때문에, "대각"이 경우의 대응의 기술
에밀리오 Garavaglia

이 모든 것을 기술로 생각하지 마십시오. 그들은 단지이다 비유
에밀리오 Garavaglia

23

작은 예에서는 다소 깔끔해 보이지만 실제 생활에서는 곧 서로를 찌를 수있는 긴 유형 세트로 끝날 것입니다. 일부는 아마도 한 방향으로 만 가능합니다.

무엇에 대한 dark_orange_node, 또는 black_and_orange_striped_node, 또는dotted_node ? 다른 색상의 점을 가질 수 있습니까? 대부분의 점이 주황색이면 찌를 수 있습니까?

그리고 새 규칙을 추가해야 할 때마다 모든 poke_adjacent함수 를 다시 방문하고 더 많은 if 문을 추가해야합니다.


항상 그렇듯이 일반적인 예제를 만드는 것은 어렵습니다.

그러나이 특정 예제 를 수행 poke()하려면 모든 클래스에 멤버를 추가하고 일부 클래스는 호출을 무시하도록합니다 (void poke() {} 관심이없는 )을 .

확실히 그것은 typeids를 비교하는 것보다 훨씬 저렴할 것 입니다.


3
당신은 "확실히"라고 말하는데 왜 그렇게 확신 하는가? 그게 제가 알아 내려고하는 것입니다. orange_node의 이름을 pokable_node로 바꾸고 poke ()를 호출 할 수있는 유일한 것입니다. 즉, 내 인터페이스는 예외를 발생시키는 poke () 메서드를 구현해야합니다 ( "이 노드는 pokable가 아닙니다"). 비싼 것 같습니다 .
mbr0wn

2
그가 예외를 던져야하는 이유는 무엇입니까? 인터페이스가 "포크 가능"인지 여부에 관심이 있다면 "isPokeable"함수를 추가하고 poke 함수를 호출하기 전에 먼저 호출하십시오. 아니면 그가 말한대로하고 "찌르지 않는 수업에서는 아무것도하지 마십시오".
브랜든

1
@ mbr0wn : 더 나은 질문은 왜 pokable 및 nonpokable 노드가 동일한 기본 클래스를 공유하기를 원하는지입니다.
Nicol Bolas

2
@NicolBolas 왜 우호적이고 적대적인 몬스터가 동일한 기본 클래스, 포커스 및 비 포커스 UI 요소 또는 숫자 키패드가있는 키보드와 숫자 키패드가없는 키보드를 공유하기를 원합니까?
user253751

1
@ mbr0wn 이것은 행동 패턴처럼 들립니다. 기본 인터페이스에는 두 가지 메서드 supportsBehaviourinvokeBehaviour있으며 각 클래스에는 동작 목록이있을 수 있습니다. 하나의 동작은 Poke이며, pokeable을 원하는 모든 클래스에 의해 지원되는 Behaviors 목록에 추가 될 수 있습니다.
Falco

20

일부 컴파일러는 사용하지 않음 / RTTI가 항상 활성화되지는 않습니다.

나는 당신이 그러한 주장을 오해했다고 믿습니다.

RTTI를 사용하지 않는 C ++ 코딩 장소가 많이 있습니다. RTTI를 강제로 비활성화하기 위해 컴파일러 스위치를 사용하는 경우. 이러한 패러다임 내에서 코딩하는 경우 ... 이미이 제한에 대한 정보를 이미 알고있을 것입니다.

따라서 문제는 라이브러리에 있습니다. 즉, RTTI에 의존하는 라이브러리를 작성하는 경우 RTTI 를 끄는 사용자 라이브러리를 사용할 수 없습니다 . 이러한 사람들이 라이브러리를 사용하도록하려면 RTTI를 사용할 수있는 사람들이 라이브러리를 사용하더라도 RTTI를 사용할 수 없습니다. 마찬가지로 중요한 것은 RTTI를 사용할 수없는 경우 RTTI 사용이 거래를 방해하기 때문에 라이브러리를 좀 더 많이 구매해야한다는 것입니다.

추가 메모리 비용 / 느릴 수 있음

핫 루프에서하지 않는 일이 많이 있습니다. 메모리를 할당하지 않습니다. 연결 목록을 반복하지 않습니다. 기타 등등. RTTI는 "여기서는하지 말 것"중 하나 일 수 있습니다.

그러나 모든 RTTI 예제를 고려하십시오. 모든 경우에, 불확정 유형의 오브젝트가 하나 이상 있고 일부에 대해 불가능할 수있는 일부 작업을 수행하려고합니다.

이는 디자인 수준 에서 해결해야하는 문제 입니다. "STL"패러다임에 맞는 메모리를 할당하지 않는 컨테이너를 작성할 수 있습니다. 연결 목록 데이터 구조를 피하거나 사용을 제한 할 수 있습니다. 구조체의 배열을 배열의 구조체 등으로 재구성 할 수 있습니다. 몇 가지 사항이 변경되지만 구획화 상태로 유지할 수 있습니다.

복잡한 RTTI 작업을 일반 가상 함수 호출로 변경 하시겠습니까? 그것은 디자인 문제입니다. 변경해야하는 경우 모든 파생 클래스를 변경해야합니다 . 많은 코드가 다양한 클래스와 상호 작용하는 방식을 변경합니다. 이러한 변경의 범위는 성능이 중요한 코드 섹션을 훨씬 넘어서 확장됩니다.

그래서 .. 왜 처음부터 잘못 썼나요?

필요하지 않은 속성이나 메서드를 정의 할 필요가 없습니다. 기본 노드 클래스는 간결하고 의미를 유지할 수 있습니다.

무엇을 위해?

당신은 기본 클래스가 "가볍고 비열하다"고 말합니다. 하지만 정말 ... 존재하지 않습니다 . 실제로 아무것도 하지 않습니다 .

귀하의 예를보십시오 : node_base. 뭔데? 인접한 다른 것들을 가진 것 같습니다. 이것은 Java 인터페이스 (이전 제네릭 Java)입니다. 사용자가 실제 유형으로 캐스트 할 수있는 것으로 만 존재하는 클래스입니다 . 인접성 (자바 추가 ToString) 과 같은 기본 기능을 추가 할 수도 있지만 그게 전부입니다.

"희박하고 평균"과 "투명"사이에는 차이가 있습니다.

Yakk이 말했듯이 이러한 프로그래밍 스타일은 모든 기능이 파생 클래스에 있으면 해당 시스템 외부의 사용자가 해당 파생 클래스에 액세스 할 수 없으므로 시스템과 상호 운용 할 수 없기 때문에 상호 운용성이 제한됩니다. 가상 기능을 재정의하고 새로운 동작을 추가 할 수 없습니다. 그들은 심지어 그 함수를 호출 할 수 없습니다 .

그러나 그들이하는 일은 심지어 시스템 내에서도 실제로 새로운 일을하는 것을 큰 고통으로 만드는 것입니다. 귀하의 poke_adjacent_oranges기능을 고려하십시오 . 누군가 s lime_node처럼 찌를 수 있는 유형을 원하면 어떻게됩니까 orange_node? 글쎄, 우리는 lime_node에서 파생 할 수 없습니다 orange_node. 말이 안 돼.

대신에서 lime_node파생 된 새 항목을 추가해야합니다 node_base. 그런 다음의 이름 poke_adjacent_orangespoke_adjacent_pokables. 그리고, 캐스트 시도 orange_node하고 lime_node; 캐스트가 작동하는 것이 우리가 찌르는 것입니다.

그러나, lime_node그것의 필요 자체 poke_adjacent_pokables . 그리고이 함수는 동일한 캐스팅 검사를 수행해야합니다.

그리고 세 번째 유형을 추가하면 자체 함수를 추가 할뿐만 아니라 다른 두 클래스의 함수를 변경해야합니다.

분명히, 이제 poke_adjacent_pokables무료 기능 을 만들어 모든 기능에 대해 작동합니다. 그러나 누군가가 네 번째 유형을 추가하고 해당 기능에 추가하는 것을 잊으면 어떻게 될까요?

안녕하세요, 조용한 파손 . 이 프로그램은 다소 괜찮은 것처럼 보이지만 그렇지 않습니다. 했다 poke되어 실제 가상 함수, 컴파일러는 당신의 순수 가상 함수를 재정의하지 않았을 때 실패했을 것이다 node_base.

당신의 방식대로, 당신은 그러한 컴파일러 검사가 없습니다. 물론 컴파일러는 순수 가상이 아닌 것을 확인하지 않지만 최소한 보호가 가능한 경우에 보호 기능이 있습니다 (즉, 기본 작업이 없음).

RTTI와 함께 투명한 기본 클래스를 사용하면 유지 관리에 악몽이 생깁니다. 실제로 대부분의 RTTI 사용은 유지 관리 문제로 이어집니다. 그렇다고 RTTI가 유용 하지 않다는 의미는 아닙니다 ( boost::any예를 들어 작업 을 수행하는 데 필수적입니다 ). 그러나 이것은 매우 특별한 요구를 위한 매우 전문화 된 도구입니다 .

그런 식으로 goto. 과 같은 방식으로 "유해"합니다 . 제거해서는 안되는 유용한 도구입니다. 그러나 코드 내에서 거의 사용되지 않습니다.


그렇다면 투명한 기본 클래스와 동적 캐스팅을 사용할 수 없다면 어떻게 뚱뚱한 인터페이스를 피할 수 있을까요? 유형에 대해 호출하려는 모든 함수가 기본 클래스로 버블 링되지 않도록하려면 어떻게해야합니까?

대답은 기본 클래스가 무엇인지에 따라 다릅니다.

같은 투명한 기본 클래스 node_base는 문제에 대해 잘못된 도구를 사용하고 있습니다. 연결된 목록은 템플릿에서 가장 잘 처리됩니다. 노드 유형과 인접성은 템플릿 유형에 의해 제공됩니다. 목록에 다형성 유형을 넣으려면 할 수 있습니다. 템플릿 인수에서와 BaseClass*같이 사용 T하십시오. 또는 선호하는 스마트 포인터.

그러나 다른 시나리오가 있습니다. 하나는 많은 일을하지만 몇 가지 선택적인 부분이있는 유형입니다. 특정 인스턴스는 특정 기능을 구현할 수 있지만 다른 인스턴스는 구현하지 않을 수 있습니다. 그러나 이러한 유형의 디자인은 일반적으로 적절한 대답을 제공합니다.

"entity"클래스가 이에 대한 완벽한 예입니다. 이 클래스는 오랫동안 게임 개발자를 괴롭 혔습니다. 개념적으로는 거의 12 개의 완전히 다른 시스템의 교차점에 사는 거대한 인터페이스를 가지고 있습니다. 그리고 엔티티마다 다른 속성이 있습니다. 일부 엔티티는 시각적 표현이 없으므로 렌더링 기능이 아무 작업도 수행하지 않습니다. 그리고 이것은 모두 런타임에 결정됩니다.

이를위한 최신 솔루션은 구성 요소 스타일 시스템입니다. Entity구성 요소 집합의 컨테이너 일 뿐이며 그 사이에 접착제가 있습니다. 일부 구성 요소는 선택 사항입니다. 시각적 표현이없는 엔티티에는 "그래픽"구성 요소가 없습니다. AI가없는 엔티티에는 "컨트롤러"구성 요소가 없습니다. 기타 등등.

이러한 시스템의 엔티티는 구성 요소에 대한 포인터 일 뿐이며 대부분의 인터페이스는 구성 요소에 직접 액세스하여 제공됩니다.

이러한 구성 요소 시스템을 개발하려면 디자인 단계에서 특정 기능이 개념적으로 함께 그룹화되어 하나를 구현하는 모든 유형이 모두 구현할 수 있음을 인식해야합니다. 이를 통해 예상 기본 클래스에서 클래스를 추출하여 별도의 구성 요소로 만들 수 있습니다.

이는 또한 단일 책임 원칙을 따르는 데 도움이됩니다. 이러한 구성 요소 화 된 클래스는 구성 요소의 소유자 일 책임 만 있습니다.


Matthew Walton에서 :

많은 답변이 귀하의 예제가 node_base가 라이브러리의 일부이고 사용자가 자신의 노드 유형을 만들 것이라고 제안한다는 생각에 주목하지 않습니다. 그러면 다른 솔루션을 허용하도록 node_base를 수정할 수 없으므로 RTTI가 최선의 선택이 될 수 있습니다.

좋습니다. 살펴 보겠습니다.

이를 이해하려면 일부 라이브러리 L이 컨테이너 또는 기타 구조화 된 데이터 보유자를 제공하는 상황이 있어야합니다. 사용자는이 컨테이너에 데이터를 추가하고 그 내용을 반복합니다. 그러나 라이브러리는 실제로이 데이터로 아무것도하지 않습니다. 단순히 그 존재를 관리합니다.

그러나 그것은 그것의 파괴 만큼 그 존재를 관리하지도 않는다 . 그 이유는 그러한 목적으로 RTTI를 사용할 것으로 예상된다면 L이 무지한 클래스를 생성하기 때문입니다. 즉 , 코드 가 개체를 할당하고 관리를 위해 L에 넘깁니다.

이제 이와 같은 것이 합법적 인 디자인 인 경우가 있습니다. 이벤트 시그널링 / 메시지 전달, 스레드로부터 안전한 작업 대기열 등 일반적인 패턴은 다음과 같습니다. 누군가가 모든 유형에 적합한 두 코드 사이에서 서비스를 수행하고 있지만 서비스가 관련된 특정 유형을 인식 할 필요는 없습니다. .

C에서는이 패턴의 철자가 void*이며 ,이 패턴을 사용하려면 깨지지 않도록 많은주의가 필요합니다. C ++에서이 패턴은 철자가 지정됩니다 std::experimental::any(곧 철자가됩니다 std::any).

이 방법 에게서는 작업에이 L가 제공하는 것입니다 node_base소요 클래스 any실제 데이터를 나타냅니다. 메시지, 스레드 큐 작업 항목 또는 수행중인 모든 작업을 수신하면 any보낸 사람과받는 사람이 모두 알고있는 적절한 유형으로 캐스트합니다 .

그래서 그 대신 유도 orange_node에서 node_data, 당신은 단순히 스틱 orange의 내부 node_dataany멤버 필드를. 최종 사용자는이를 추출 any_cast하여 orange. 캐스트가 실패하면 orange.

이제의 구현에 익숙하다면 any"잠깐만 기다려주세요. any 내부적으로 RTTI를 사용하여 any_cast작업을 수행합니다." 라고 말할 수 있습니다. 내가 대답하는 "... 예".

이것이 추상화 의 요점입니다 . 세부 사항에서 누군가 RTTI를 사용하고 있습니다. 그러나 운영해야하는 수준에서 직접 RTTI는해야 할 일이 아닙니다.

원하는 기능을 제공하는 유형을 사용해야합니다. 결국, 당신은 정말로 RTTI를 원하지 않습니다. 원하는 것은 주어진 유형의 값을 저장하고 원하는 대상을 제외한 모든 사람에게 숨긴 다음 저장된 값이 실제로 해당 유형인지 확인하여 해당 유형으로 다시 변환 할 수있는 데이터 구조입니다.

라고 any합니다. 그것은 사용 RTTI, 그러나 사용은 any더 정확하게 원하는 의미 적합하기 때문에, 직접 RTTI를 사용하여 훨씬 우수하다.


10

함수를 호출하면 원칙적으로 어떤 정확한 단계를 밟을 지 신경 쓰지 않고 특정 제약 조건 내에서 일부 상위 수준 목표를 달성 할 수 있습니다 (그리고 함수가이를 발생시키는 방법은 실제로 자체 문제입니다).

RTTI를 사용하여 특정 작업을 수행 할 수있는 특수 개체를 미리 선택하는 반면 동일한 세트의 다른 개체는 할 수없는 경우 편안한 세계관을 깨는 것입니다. 갑자기 전화를 건 사람은 자신의 하수인에게 그 일을 계속하라고 말하는 대신 누가 무엇을 할 수 있는지 알아야합니다. 어떤 사람들은 이것에 신경을 쓰고 있으며 이것이 RTTI가 약간 더러워진 것으로 간주되는 이유의 큰 부분이라고 생각합니다.

성능 문제가 있습니까? 어쩌면 나는 그것을 경험 한 적이 없으며, 20 년 전의 지혜 일 수도 있고, 2 개가 아닌 3 개의 조립 지침을 사용하는 것이 용납 할 수없는 팽창이라고 정직하게 믿는 사람들의 지혜 일 수도 있습니다.

따라서 어떻게 처리 할 것인가 ... 상황에 따라 노드 별 속성을 별도의 개체로 묶는 것이 합리적 일 수 있습니다 (즉, 전체 '주황색'API가 별도의 개체가 될 수 있음). 그런 다음 루트 개체는 'orange'API를 반환하는 가상 함수를 가질 수 있으며, 주황색이 아닌 개체에 대해서는 기본적으로 nullptr을 반환합니다.

상황에 따라 과도 할 수 있지만 특정 노드가 특정 API를 지원하는지 여부를 루트 수준에서 쿼리하고 지원하는 경우 해당 API에 특정한 함수를 실행할 수 있습니다.


6
Re : 성능 비용-dynamic_cast <>를 3GHz 프로세서의 앱에서 약 2µs의 비용으로 측정했는데, 이는 열거 형을 확인하는 것보다 약 1000 배 더 느립니다. (우리는 마이크로에 대해 많은 관심 그래서 우리의 응용 프로그램은, 11.1ms 메인 루프 마감이 있습니다.)
Crashworks

6
성능은 구현에 따라 많이 다릅니다. GCC는 빠른 typeinfo 포인터 비교를 사용합니다. MSVC는 빠르지 않은 문자열 비교를 사용합니다. 그러나 MSVC의 메서드는 라이브러리의 다른 버전, 정적 또는 DLL에 연결된 코드와 함께 작동합니다. 여기서 GCC의 포인터 메서드는 정적 라이브러리의 클래스가 공유 라이브러리의 클래스와 다르다고 생각합니다.
Zan Lynx

1
@Crashworks 여기에 완전한 기록을 남기기 위해 : 어떤 컴파일러 (및 어떤 버전)였습니까?
H. Guijt

@Crashworks는 어떤 컴파일러가 관찰 된 결과를 생성했는지에 대한 정보를 요청합니다. 감사.
underscore_d

@underscore_d : MSVC.
Crashworks

9

C ++는 정적 유형 검사 개념을 기반으로합니다.

[1] 이며, RTTI, dynamic_casttype_id동적 타입 검사이다.

따라서 본질적으로 정적 유형 검사가 동적 유형 검사보다 선호되는 이유를 묻습니다. 그리고 간단한 대답은 정적 유형 검사가 동적 유형 검사보다 바람직한 지 여부는 . 많이. 그러나 C ++는 정적 유형 검사 개념을 중심으로 설계된 프로그래밍 언어 중 하나입니다. 그리고 이것은 예를 들어 개발 프로세스, 특히 테스트가 일반적으로 정적 유형 검사에 적용되고 가장 적합하다는 것을 의미합니다.


템플릿이나 다른 방법으로이 작업을 수행하는 깨끗한 방법을 모를 것입니다.

정적 유형 검사를 사용하고 방문자 패턴을 통해 어떠한 캐스팅도 수행하지 않는 그래프의 이종 노드 프로세스를 수행 할 수 있습니다. 예를 들면 다음과 같습니다.

#include <iostream>
#include <set>
#include <initializer_list>

namespace graph {
    using std::set;

    class Red_thing;
    class Yellow_thing;
    class Orange_thing;

    struct Callback
    {
        virtual void handle( Red_thing& ) {}
        virtual void handle( Yellow_thing& ) {}
        virtual void handle( Orange_thing& ) {}
    };

    class Node
    {
    private:
        set<Node*> connected_;

    public:
        virtual void call( Callback& cb ) = 0;

        void connect_to( Node* p_other )
        {
            connected_.insert( p_other );
        }

        void call_on_connected( Callback& cb )
        {
            for( auto const p : connected_ ) { p->call( cb ); }
        }

        virtual ~Node(){}
    };

    class Red_thing
        : public virtual Node
    {
    public:
        void call( Callback& cb ) override { cb.handle( *this ); }

        auto redness() -> int { return 255; }
    };

    class Yellow_thing
        : public virtual Node
    {
    public:
        void call( Callback& cb ) override { cb.handle( *this ); }
    };

    class Orange_thing
        : public Red_thing
        , public Yellow_thing
    {
    public:
        void call( Callback& cb ) override { cb.handle( *this ); }

        void poke() { std::cout << "Poked!\n"; }

        void poke_connected_orange_things()
        {
            struct Poker: Callback
            {
                void handle( Orange_thing& obj ) override
                {
                    obj.poke();
                }
            } poker;

            call_on_connected( poker );
        }
    };
}  // namespace graph

auto main() -> int
{
    using namespace graph;

    Red_thing   r;
    Yellow_thing    y1, y2;
    Orange_thing    o1, o2, o3;

    for( Node* p : std::initializer_list<Node*>{ &y1, &y2, &r, &o2, &o3 } )
    {
        o1.connect_to( p );
    }
    o1.poke_connected_orange_things();
}

이것은 노드 유형 세트가 알려져 있다고 가정합니다.

그렇지 않은 경우 방문자 패턴 (다양한 변형이 있음)은 몇 가지 중앙 집중식 캐스트 또는 단일 캐스트로 표현할 수 있습니다.


템플릿 기반 접근 방식은 Boost Graph 라이브러리를 참조하십시오. 내가 익숙하지 않다고 말해서 슬프고 나는 그것을 사용하지 않았습니다. 그래서 정확히 무엇을, 어떻게, 그리고 RTTI 대신 정적 유형 검사를 어느 정도 사용하는지 확실하지 않지만 Boost는 일반적으로 정적 유형 검사를 중심 아이디어로 템플릿 기반이므로 Graph 하위 라이브러리도 정적 유형 검사를 기반으로합니다.


[1] 런타임 유형 정보 .


1
주목해야 할 한 가지 "재미있는 점"은 방문자 패턴에 필요한 코드의 양 (유형을 추가 할 때 변경됨)을 줄일 수 있다는 것입니다. RTTI를 사용하여 계층 구조를 "등반"하는 것입니다. 나는 이것을 "비순환 적 방문자 패턴"으로 알고있다.
Daniel Jour

3

물론 다형성이 도움이되지 않는 시나리오가 있습니다 : 이름. typeid이 이름이 인코딩되는 방식은 구현에서 정의되지만 유형의 이름에 액세스 할 수 있습니다. 그러나 두 개의 typeid-s를 비교할 수 있기 때문에 일반적으로 이것은 문제가되지 않습니다 .

if ( typeid(5) == "int" )
    // may be false

if ( typeid(5) == typeid(int) )
   // always true

해시도 마찬가지입니다.

[...] RTTI는 "유해한 것으로 간주"

유해는 RTTI는 몇 가지 단점을 가지고 있지만 : 확실히 과장입니다 않습니다 도 장점이있다.

실제로 RTTI를 사용할 필요는 없습니다. RTTI는 OOP 문제를 해결 하는 도구입니다 . 다른 패러다임을 사용하면 사라질 가능성이 높습니다. C에는 RTTI가 없지만 여전히 작동합니다. 대신 C ++는 OOP를 완벽하게 지원하고 런타임 정보가 필요할 수있는 몇 가지 문제를 극복 할 수있는 여러 도구를 제공합니다 . 그중 하나 실제로 RTTI이지만 가격이 함께 제공됩니다. 만약 당신이 그것을 감당할 수 없다면, 안전한 성능 분석 후에 만 ​​말하는 것이 더 좋을 것입니다. 여전히 구식이 있습니다 void*. 그것은 무료입니다. 비용이 들지 않습니다. 그러나 형식 안전성은 없습니다. 그래서 그것은 모두 거래에 관한 것입니다.


  • 일부 컴파일러는 사용하지 않습니다. / RTTI가 항상 활성화되어
    있지는 않습니다. 저는이 인수를 구매하지 않습니다. C ++ 14 기능을 지원하지 않는 컴파일러가 있기 때문에 C ++ 14 기능을 사용하지 말아야한다고 말하는 것과 같습니다. 그러나 아무도 C ++ 14 기능을 사용하는 것을 낙담하지 않을 것입니다.

(엄격한) C ++ 코드를 작성하면 구현에 관계없이 동일한 동작을 기대할 수 있습니다. 표준 준수 구현은 표준 C ++ 기능을 지원해야합니다.

그러나 일부 환경에서는 C ++가 정의 ( "독립")하고 RTTI를 제공 할 필요가 없으며 예외도 virtual마찬가지라는 점을 고려하십시오. RTTI는 ABI 및 실제 유형 정보와 같은 낮은 수준의 세부 정보를 처리하는 기본 계층이 올바르게 작동해야합니다.


이 경우 RTTI와 관련하여 Yakk에 동의합니다. 예, 사용할 수 있습니다. 하지만 논리적으로 맞습니까? 언어가이 검사를 우회 할 수 있다고해서 반드시 수행해야한다는 의미는 아닙니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.