2017-02-06 3 views
0

Я использую наследование только ради повторного использования кода, я никогда не бросал класс в его базовый класс. Это высокопроизводительная программа, поэтому я хотел бы избежать использования функций virtual, но я не знаю, как это сделать. Рассмотрим следующий кодНенужное использование виртуальных функций только для повторного использования кода

class A{ 
public: 
    void print(){ 
    std::cout << "Result of f() is: " << f() << std::endl; 
    } 

    virtual std::string f(){ 
    return "A"; 
    } 
}; 

class B : public A{ 
public: 
    virtual std::string f(){ 
    return "B"; 
    } 
}; 

Будет ли это быть как-то можно не использовать virtual функции для функции f() и не переопределять функцию print() в классе B? Мне все равно, что A является базовым классом B Я просто не хочу записывать f() еще раз. Наверное, наследование - это не путь, возможно, шаблоны можно использовать разумно, но я понятия не имею.

+3

Наследование не является отличным способом повторного использования кода. –

+2

Если вы хотите избежать виртуального полиморфизма, найдите CRTP. Однако влияние производительности на vtables в большинстве случаев является чрезмерным. –

+6

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

ответ

3

CRTP pattern обычно используется, чтобы избежать динамической диспетчеризации, когда это может быть статически определил, какой метод реализации вызывает конкретный метод.

В вашем примере как A, так и B наследуются от одного базового класса, который предоставляет метод print(). Базовый класс, назовем его Print, является шаблоном, аргументом шаблона которого является класс, который предоставляет f(). Твист, который заработал этот шаблон, «любопытный» прозвище, заключается в том, что подклассы должны наследовать базовый класс , templatized по подклассу. Это позволяет подклассам получить доступ к методу print базового класса, но получить версию базового класса - и, в свою очередь, версию print -, которая вызывает свои собственные f.

Вот пример рабочего кода:

#include <iostream> 

template<typename F> 
class Print { 
public: 
    void print() { 
    F& final = static_cast<F&>(*this); 
    std::cout << "Result of f() is: " << final.f() << std::endl; 
    } 
}; 

class A: public Print<A> { 
public: 
    std::string f(){ 
    return "A"; 
    } 
}; 

class B: public Print<B> { 
public: 
    std::string f(){ 
    return "B"; 
    } 
}; 


int main() { 
    A a; 
    B b; 
    a.print(); 
    b.print(); 
} 

Хотя print реализации повторно среди A и B, нет виртуальных методов здесь, ни там виртуальной (времени выполнения) отправка или run- проверки времени. Один из присутствующих представляет собой static_cast<>, чья безопасность должным образом проверена компилятором.

Это возможно, потому что для каждого использования Print<F> компилятор точно знает, что такое F. Таким образом, Print<A>::print, как известно, вызывает A::f, а Print<B>::print вызывает B::f, все известные во время компиляции. Это позволяет компилятору встроить и иным образом оптимизировать такие вызовы, как и любые другие вызовы не виртуальных методов.

Недостатком является то, что наследования нет. Обратите внимание, что B не получен из A - если бы это было так, шаблон не работал бы, и оба A::print и B::print напечатали A, так как это то, что Print<A> выходов. Более фундаментально, вы не может пройти B*, где ожидается A* - это неопределенное поведение. Фактически, A и B не имеют общего суперкласса, классы Parent<A> и Parent<B> полностью разделены. Потеря диспетчеризации во время выполнения с ее недостатками и преимуществами и возможностью статической отправки вместо этого является фундаментальным компромиссом статического полиморфизма.

+1

Отлично! Это именно то, что я искал. – tom

+0

@tom Удачи с C++! – user4815162342

+0

Один вопрос: Может ли вызов 'final.f()' быть встроенным? – tom

0

Если по какой-либо причине вы не хотите «накладные расходы» динамического связывания, вы можете опустить virtual -keyword, что заставляет компилятор использовать статическое связывание и отключение полиморфизма. То есть, компилятор будет связывать реализацию исключительно на основе типа переменной во время компиляции, а не во время выполнения.

Честно говоря, я никогда не был в ситуации, когда я определил конкретную реализацию метода в подклассе, не включив полиморфизм. Обычно это указывает на отсутствие определенного поведения, то есть метод не должен быть переопределен в подклассе вообще.

Далее, код, инкапсулирующий константу символа в строковый объект, имеет гораздо большую производительность, чем динамическое связывание/vtable - «накладные расходы». Действительно, переосмыслите его дважды, а затем измерьте увеличение производительности перед выполнением таких «оптимизаций».

В любом случае, посмотрите, как ведет себя ваш код, если вы опустите virtual. Обратите внимание, что ptr->f() в коде связан с A::f, так как тип переменной является A*, хотя это указывает на объект типа B:

class A{ 
public: 
    void print(){ 
     std::cout << "Result of f() is: " << f() << std::endl; 
    } 

    std::string f(){ 
     return "A"; 
    } 
}; 

class B : public A{ 
public: 
    std::string f(){ 
     return "B"; 
    } 
}; 

int main() 
{ 
    A a; cout << a.f(); // -> yields "A" 
    B b; cout << b.f(); // -> yields "B" 
    A* ptr = &b; cout << ptr->f(); // -> yields "A"; (virtual f, in contrast) would be "B" 

    return 0; 
} 
0

Вы можете использовать шаблоны, чтобы выбрать динамические или нединамические версии A и B. Довольно сложный/уродливый вариант, но стоит рассмотреть.

#include <string> 

template <bool Virt = false> 
class A{ 
public: 
    std::string f(){ 
    return "A"; 
    } 
}; 

template <> 
class A<true> : A<false>{ 
public: 
    virtual std::string f(){ 
    return A<false>::f(); 
    } 
}; 

template <bool Virt = false> 
class B : public A<Virt>{ 
public: 
    std::string f(){ 
    return "B"; 
    } 
}; 

std::string f1() { return B<>().f(); } 

std::string f2(A<true> &a) { return a.f(); } 

std::string f3() { B<true> b; return f2(b); } 

#include <iostream> 

int main(){ 

std::cout << f1() << '\n'; 

std::cout << f3() << '\n'; 

return(0); 
} 

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

Смежные вопросы