4

在 C++ 中,有没有办法让“抽象”基类方法(,从基类声明和调用,但在子类中实现)而不将方法声明为virtual

当然,这个问题只适用于不需要多态性的情况(从未使用过的基类型的指针/引用)。考虑以下:

#define NO_OPT asm volatile ("");  // to prevent some compiler optimization

template<typename DerivedType>
void doSomething(DerivedType& d) { d.foo(); }

namespace test1 {

    struct Base {
        inline void foo()
        {
            // ... do common stuff pre-call ...
            foo_impl();
            // ... do common stuff post-call ...
        }
        virtual void foo_impl() = 0; // the abstract method
    };
    struct D1 : public Base { virtual void foo_impl() final { NO_OPT } };
    struct D2 : public Base { virtual void foo_impl() final { NO_OPT } };

    // Then the usage of D1, D2, ..., DN, could be as follows:

    void go() {

        D1 d1; doSomething(d1);
        D2 d2; doSomething(d2);

        for ( long i=0; i < 5000000000; i++ ) {
            // this loop takes [9] seconds
            doSomething(d2);
        }
    }
}

请注意,在这种情况下不需要多态性并且编译器有很多优化机会。

g++但是,我在最新的(4.8.2) 和clang(3.4)中对这段代码进行了基准测试,并-O3启用了优化,包括链接时间 (LTO),它比以下替代实现慢得多(使用模板而不是虚拟方法):

namespace test2 {

    template<typename DerivedType>
    struct Base : public DerivedType  // inheritance in reverse
    {
        inline void foo()
        {
            // ... do common stuff pre-call ...
            DerivedType::foo_impl();
            // ... do common stuff post-call ...
        }
    };
    struct D1 { void foo_impl() { NO_OPT } };
    struct D2 { void foo_impl() { NO_OPT } };

    void go() {
        Base<D1> d1; doSomething(d1);
        Base<D2> d2; doSomething(d2);

        for ( long i=0; i < 5000000000; i++ ) {
            // this loop takes [3] seconds
            doSomething(d2);
        }
    }
}

g++并且clang非常一致,每次编译优化代码需要 9 秒来执行test1循环,但只需要 3 秒来执行test2循环。因此,即使test1逻辑不需要动态调度,因为所有调用都应该能够在编译时解决,但它仍然明显慢得多。

所以重申我的问题:当不需要多态性时,有没有办法使用方便的直接类继承(如 in )来实现这种“抽象方法”行为test1,但没有虚函数的性能损失(如在test2)?

4

3 回答 3

5

为什么要显式调用运行时支持来解决您没有并且不想被认为是直截了当的情况?这也会让人类感到困惑。模板是完成这项工作的完美工具。

你想要一个通用的接口。鉴于您的示例模板实现,您甚至不需要拥有它的类型之间的任何明显关系,只是它们确实拥有它。

经典的CRTP方式是

template<class d>
struct b { 
    void foo() { bark(); static_cast<d*>(this)->foo_impl(); bite(); }
};

struct d1 : b<d1> { void foo_impl() { fuzzbang("d1"); } };
struct d2 : b<d2> { void foo_impl() { fizzbuzz("d2"); } };

人们以前会见过的,它是按要求惯用的,但是您的

class m1 { protected: void foo_impl() { f1("1"); } };
class m2 { protected: void foo_impl() { f2("2"); } };

template<class m>
struct i : private m { void foo() { bark(); m::foo_impl(); bite(); } };

也有很多值得推荐的地方:不需要强制转换,而且意图很明确。


我认为您无法合法地将具有嵌入式易失性访问的代码上的优化器结果与没有它们的类似代码上的结果进行比较。在您的实际代码中尝试这两种实现。执行 50 亿次的任何事情的性能几乎肯定会受到缓存影响而不是指令数的支配。

于 2013-08-15T02:35:04.840 回答
3

如果函数具有多态值(多态类型的引用或指针),并且它virtual通过该变量调用函数,则编译器必须假定这可能是该类或从它派生的任何类。而且由于多态性不允许编译器知道是否有从它派生的类,它必须使用虚拟调度来调用函数。

只有一种方法可以避免在通过多态值调用虚函数时使用虚拟调度:直接调用特定类型的函数就像您在案例 #2 中所做的那样。

你把这个电话放在哪里取决于你。您可以将调用放入foo,您可以derived_foo使用人们用来调用派生类版本的替代函数等。但是,您想这样做,最终必须virtual以不同的方式调用该函数。这需要使用不同的代码路径。

如果提供一个final类的多态变量,编译器可能会将virtual对该变量的所有调用优化为非虚拟调度。但是,这不是标准所要求的行为。每个编译器可能会或可能不会实现该优化,因此除非它变得普遍,否则您不能指望它。

这个问题看起来很像一个 XY 问题。您正在尝试解决一些问题,并且您选择了一个带有虚函数的基类,以及一个您将始终使用的派生类。这个问题最好通过CRTP解决,它根本不使用虚函数。

于 2013-08-15T00:26:48.160 回答
1

如果您不需要动态多态,请不要virtual在代码中使用关键字:

#define NO_OPT asm volatile ("");  // to prevent some compiler optimization

template<typename T>
void doSomething(T& d) { d.foo(); }

namespace test1 
{
    struct Base
    {
    protected:
        void prior() {}
        void after() {}
    };

    struct D1
        :public Base 
    { 
        void foo() 
        {
            prior();
            NO_OPT
            after();
        }
    };

    struct D2
        :public Base
    { 
        void foo() 
        {
            prior();
            NO_OPT
            after();
        }
    };

    void go() {

        D1 d1; doSomething(d1);
        D2 d2; doSomething(d2);

        for ( long i=0; i < 5000000000; i++ ) {
            doSomething(d2);
        }
    }
}
于 2013-08-15T00:30:34.067 回答