2015-12-09 3 views
1

Мой вопрос относится к What's the point of IsA() in C++?. У меня есть критический код производительности, который содержит при определенном методе обработки определенных функций из производных классов, где доступен только базовый указатель. Каков наилучший способ проверить, какой производный класс у нас есть? Я закодировал два варианта, во втором варианте я мог бы удалить список Animal_type и функцию get_type().Внедрить идентификатор для класса или использовать dynamic_cast

#include <iostream> 

enum Animal_type { Dog_type, Cat_type }; 

struct Animal 
{ 
    virtual Animal_type get_type() const = 0; 
}; 

struct Dog : Animal 
{ 
    void go_for_walk() const { std::cout << "Walking. Woof!" << std::endl; } 
    Animal_type get_type() const { return Dog_type; } 
}; 

struct Cat : Animal 
{ 
    void be_evil() const { std::cout << "Being evil!" << std::endl; } 
    Animal_type get_type() const { return Cat_type; } 
}; 

void action_option1(Animal* animal) 
{ 
    if (animal->get_type() == Dog_type) 
     dynamic_cast<Dog*>(animal)->go_for_walk(); 
    else if (animal->get_type() == Cat_type) 
     dynamic_cast<Cat*>(animal)->be_evil(); 
    else 
     return; 
} 

void action_option2(Animal* animal) 
{ 
    Dog* dog = dynamic_cast<Dog*>(animal); 
    if (dog) 
    { 
     dog->go_for_walk(); 
     return; 
    } 

    Cat* cat = dynamic_cast<Cat*>(animal); 
    if (cat) 
    { 
     cat->be_evil(); 
     return; 
    } 

    return; 
} 

int main() 
{ 
    Animal* cat = new Cat(); 
    Animal* dog = new Dog(); 

    action_option1(cat); 
    action_option2(cat); 

    action_option1(dog); 
    action_option2(dog); 

    return 0; 
} 
+0

Вы оценили эффективность обоих подходов? – TartanLlama

+3

Почему бы не использовать старую динамическую диспетчеризацию с виртуальными функциями? Кроме того, коммутация типов обычно считается огромным запахом кода. Вы считали посетителя, стратегию или шаблонный метод? – Jens

+0

почему бы не иметь action_option1 и action_option2 как виртуальные на Animal и заставить их называть go_for_walk/be_evil? – paulm

ответ

2

В значительной степени зависит от , так как критически важный для вашей производительности код. Я видел установки, где даже динамическая диспетчеризация виртуальных функций была слишком дорогостоящей, поэтому, если вы находитесь на такой территории, забудьте о dynamic_cast и о том, как что-то сделать.

Предположим, вы в порядке с виртуальным вызовом или двумя. Вероятно, вы захотите избавиться от dynamic_cast, так как обычно это намного медленнее, чем динамическая отправка.

Прямо сейчас, у вас есть N классов, производных от общей базы и M точки в коде, где вам нужно принять решение, основанное на конкретной производной класса. Возникает вопрос: какой из N, M, скорее всего, изменится в будущем? Скорее всего, вы добавите новые производные классы или представите новые точки, в которых важны вопросы, связанные с типом? Этот ответ определит лучший дизайн для вас.

Если вы собираетесь добавлять новые классы, но количество мест, различающихся по типу, фиксировано (и в идеале такое же маленькое), подход перечисления будет лучшим выбором. Просто используйте static_cast вместо dynamic_cast; если вы знаете фактический тип времени выполнения, вам не нужно обращаться к RTTI, чтобы выполнить преобразование для вас (если не задействованы виртуальные базы и более глубокая иерархия наследования).

С другой стороны, если классы списка фиксированы, но, скорее всего, будут введены новые операции распознавания типа (или их слишком много), рассмотрите вместо этого Visitor pattern.Дайте Animal класс виртуального посетителя-акцепторной функции:

virtual void accept(AnimalVisitor &v) = 0; 

struct AnimalVisitor 
{ 
    virtual void visit(Dog &dog) = 0; 
    virtual void visit(Cat &cat) = 0; 
};  

Затем каждый производный класс будет реализовать:

void Dog::accept(AnimalVisitor &v) 
{ 
    v.visit(*this); 
} 

void Cat::accept(AnimalVisitor &v) 
{ 
    v.visit(*this); 
} 

И ваши операции будут просто использовать:

void action(Animal *animal) 
{ 
    struct Action : AnimalVisitor 
    { 
    void visit(Dog &d) override { d.go_for_walk(); } 
    void visit(Cat &c) override { c.be_evil(); } 
    }; 

    AnimalVisitor v; 

    animal->accept(v); 
} 

Если вы собираетесь добавлять как новые производные классы, так и новые операции, вы можете добавить не-абстрактные функции к вышеуказанному посетителю, чтобы существующий код, который не должен знать abo ут новые классы не нарушает:

struct AnimalVisitor 
{ 
    virtual void visit(Dog &d) = 0; 
    virtual void visit(Cat &c) = 0; 
    virtual void visit(Parrot &p) {} 
}; 
+0

Вы оценили, действительно ли разница между виртуальной отправкой и использованием тега типа? Динамическая отправка - это всего лишь одно косвенное направление, а тегу типа требуется ветка. Я помню старый документ о динамических реализациях диспетчеризации в JVM, и там это действительно зависело от количества подклассов, фактического распределения типов (если вы берете одну и ту же ветвь каждый раз, когда предсказание филиала будет хорошим, если у вас разные типы каждый раз, когда это заставит вас страдать) и, конечно же, аппаратное обеспечение. – Jens

+0

@Jens Нет, я не сделал никаких измерений. Я пропустил создание 'action' виртуальной функции в классе по двум причинам: 1), ваш ответ охватывает это; 2), он загрязняет класс чем-то, что потенциально может быть внешним. Да, действительно есть случаи, когда это делает виртуальную функцию подходящей; но я вроде как предполагал, что ОП знал об этой возможности. Тем не менее, оба решения, которые я предлагаю, требуют только общей поддержки от класса, ничего конкретного «действия». – Angew

2

Я хочу процитировать принятый ответ на вопрос, вы со ссылкой:

В современном C++ нет никакого смысла.

Для примера, самым простым решением является использование динамической диспетчеризации:

struct Animal { 
    virtual void action() = 0; 
}; 

struct Dog{ 
    virtual void action() { std::cout << "Walking. Woof!" << std::endl; } 
}; 

struct Animal { 
    virtual void action() { std::cout << "Being evil!" << std::endl; } 
}; 

int main() 
{ 
    Animals* a[2] = {new Cat(), new Dog()}; 
    a[0]->action(); 
    a[1]->action(); 
    delete a[0]; 
    delete a[1]; 
    return 0; 
} 

Для более сложных сценариев, вы можете рассмотреть шаблоны проектирования, такие как стратегия, метод шаблона или Visitor.

Если это действительно является узким местом, это может помочь объявить и Cat как final.

+0

Что вы имеете в виду как объявление? И что это делает? – Chiel

+0

@Chiel Класс, объявленный как ['final'] (http://en.cppreference.com/w/cpp/language/final), не может быть получен. – Angew

+0

@ Аngew и делает ли динамический бросок намного быстрее? – Chiel

1

Вашего первый вариант будет быстрее, но только если вы исправить ошибочную dynamic_cast (она должна быть static_cast):

void action_option1_fixed(Animal* animal) 
{ 
    if (animal->get_type() == Dog_type) 
     static_cast<Dog*>(animal)->go_for_walk(); 
    else if (animal->get_type() == Cat_type) 
     static_cast<Cat*>(animal)->be_evil(); 
} 

Точку с помощью ручной отправки на get_type() вот то, что он позволяет вам избежать дорогостоящего вызова __dynamic_cast в среде выполнения C++. Как только вы сделаете этот звонок во время выполнения, вы проиграете.

Если вы используете final спецификатор на обоих Dog и Cat (то есть, по каждому классу в вашей программе, что вы знаете, никогда не будет иметь дочерних классов), то вы будет иметь достаточно информации, чтобы знать, что

dynamic_cast<Dog*>(animal) 

может быть реализовано как простое сравнение указателей; но, к сожалению (с 2017 года) ни GCC, ни Clang не реализуют такую ​​оптимизацию. Вы можете сделать оптимизацию вручную, не вводя метод get_type, используя оператор C++ typeid:

void action_option3(Animal* animal) 
{ 
    static_assert(std::is_final_v<Dog> && std::is_final_v<Cat>, ""); 
    if (typeid(*animal) == typeid(Dog)) 
     static_cast<Dog*>(animal)->go_for_walk(); 
    else if (typeid(*animal) == typeid(Cat)) 
     static_cast<Cat*>(animal)->be_evil(); 
} 

Компиляция с clang++ -std=c++14 -O3 -S должен показать вам преимущество третьего подхода здесь.

action_option1 начинается с

movq %rdi, %rbx 
    movq (%rbx), %rax 
    callq *(%rax) 
    cmpl $1, %eax 
    jne  LBB0_1 
    movq [email protected](%rip), %rsi 
    movq [email protected](%rip), %rdx 
    xorl %ecx, %ecx 
    movq %rbx, %rdi 
    callq ___dynamic_cast 
    movq %rax, %rdi 
    addq $8, %rsp 
    popq %rbx 
    popq %rbp 
    jmp  __ZNK3Dog11go_for_walkEv ## TAILCALL 

action_option1_fixed улучшает его

movq %rdi, %rbx 
    movq (%rbx), %rax 
    callq *(%rax) 
    cmpl $1, %eax 
    jne  LBB2_1 
    movq %rbx, %rdi 
    addq $8, %rsp 
    popq %rbx 
    popq %rbp 
    jmp  __ZNK3Dog11go_for_walkEv ## TAILCALL 

(обратите внимание, что в стационарной версии, вызов __dynamic_cast ушел, уступив место только небольшой указатель по математике).

action_option2 на самом деле короче action_option1, потому что он не добавляет виртуальный вызов на вершине__dynamic_cast, но это все-таки ужасно:

movq %rdi, %rbx 
    testq %rbx, %rbx 
    je  LBB1_3 
    movq [email protected](%rip), %rsi 
    movq [email protected](%rip), %rdx 
    xorl %ecx, %ecx 
    movq %rbx, %rdi 
    callq ___dynamic_cast 
    testq %rax, %rax 
    je  LBB1_2 
    movq %rax, %rdi 
    addq $8, %rsp 
    popq %rbx 
    popq %rbp 
    jmp  __ZNK3Dog11go_for_walkEv ## TAILCALL 

И вот action_option3. Достаточно того, что я могу просто вставить все определение функции здесь мало, а excerpting:

__Z14action_option3P6Animal: 
    testq %rdi, %rdi 
    je  LBB3_4 
    movq (%rdi), %rax 
    movq -8(%rax), %rax 
    movq 8(%rax), %rax 
    cmpq [email protected](%rip), %rax 
    je  LBB3_5 
    cmpq [email protected](%rip), %rax 
    je  LBB3_6 
    retq 
LBB3_5: 
    jmp  __ZNK3Dog11go_for_walkEv ## TAILCALL 
LBB3_6: 
    jmp  __ZNK3Cat7be_evilEv  ## TAILCALL 
LBB3_4: 
    pushq %rbp 
    movq %rsp, %rbp 
    callq ___cxa_bad_typeid 

__cxa_bad_typeid хлама в конце, потому что это может быть так, что animal == nullptr.Вы можете устранить этот треск, сделав свой параметр типа Animal& вместо Animal*, чтобы компилятор знал, что он не равен нулю.

Я попытался добавить эту строку в верхней части функции:

if (animal == nullptr) __builtin_unreachable(); 

но, к сожалению, реализация звоном по typeid не подобрать на эту подсказку.