抽象类作为接口,不带虚函数表

Abstract class as an interface, without the vtable

本文关键字:函数 接口 抽象类      更新时间:2023-10-16

我想创建一个抽象类,定义一个类的一些方法。其中一些应该由基类(base)实现,一些应该在base中定义但被Derived覆盖,其他应该在base中纯虚的以强制在Derived中定义。

这当然是抽象类的作用。然而,我的应用程序只会直接使用派生对象。因此,编译器应该在编译时准确地知道要使用哪些方法。

现在,因为这段代码将在RAM非常有限的微控制器上运行,所以我希望避免实际使用虚函数表的虚拟类。从我的测试来看,编译器似乎足够聪明,除非必须,否则不会创建虚变量表,至少在某些情况下是这样。然而,我被告知永远不要相信编译器:有可能使这成为编译的必要条件吗?

下面是一些代码示例:

class Base {
  public:
    Base() {}
    virtual ~Base() {};
    virtual int thisMustBeDefined() = 0;
    virtual int thisCouldBeOverwritten() { return 10; }
    int thisWillBeUsedAsIs() { return 999; }
};
class Derived : public Base {
  public:
    Derived() {}
    ~Derived() {}
    int thisMustBeDefined() { return 11; }
};
没有vtable

没有变量表,这是我想要的

int main() {
  Derived d;
  d.thisMustBeDefined();
}

是vtable 1

由于我的草率编码,我错误地迫使编译器使用多态性,因此需要一个虚函数表。如何使这种情况抛出错误?

int main() {
  Base * d;
  d = new Derived();
  d->thisMustBeDefined();
}

是vtable 2

这里我没有在任何时候引用类"Base",所以编译器应该知道所有的方法都是在编译时预先确定的。但是,它仍然创建一个虚函数表。这是另一个例子,说明为什么我希望能够通过编译错误检测到这一点。

int main() {
  Derived * d;
  d = new Derived();
  d->thisMustBeDefined();
}

换句话说,如果我编写的代码导致编译器为我的类生成虚函数表,即使用多态性,我希望它是一个编译器错误。

正如在评论中已经提到的,您可以使用CRTP(又名静态多态性)来避免创建虚值表:

template <typename Der>
class Base {
  public:
    Base() {}
    ~Base() {};
    int thisMustBeDefined() {
        // Will fail to compile if not declared in Der
        static_cast<Der*>(this)->thisMustBeDefined();
    }
    int thisCouldBeOverwritten() { return 10; }
    int thisWillBeUsedAsIs() { return 999; }
};
class Derived : public Base<Derived> {
  public:
    Derived() {}
    ~Derived() {}
    int thisMustBeDefined() { return 11; }
    // Works since you call Derived directly from main()
    int thisCouldBeOverwritten() { return 20; }
};

如果一个函数没有在Derived中实现,为了使编译器错误更可读,你可以使用一个简单的静态检查,如以下答案所示:

#define DEFINE_HAS_SIGNATURE(traitsName, funcName, signature)               
    template <typename U>                                                   
    class traitsName                                                        
    {                                                                       
    private:                                                                
        template<typename T, T> struct helper;                              
        template<typename T>                                                
        static std::uint8_t check(helper<signature, &funcName>*);           
        template<typename T> static std::uint16_t check(...);               
    public:                                                                 
        static                                                              
        constexpr bool value = sizeof(check<U>(0)) == sizeof(std::uint8_t); 
    }
DEFINE_HAS_SIGNATURE(thisMustBeDefined, T::thisMustBeDefined, int(*)(void));

并在Base构造函数中添加静态检查:

Base() {
    static_assert(thisMustBeDefined<Der>::thisMustBeDefined, 
                  "Derived class must implement thisMustBeDefined");
}

尽管在小型设备上工作时应该考虑一个缺点,并且一次有更多版本的Derived,但Base中的代码将为每个Derived实例复制。

所以你必须决定哪个限制对你的用例来说更重要。

正如@ChrisDrew在他们的评论中指出的那样,将thisCouldBeOverwritten()thisWillBeUsedAsIs()函数移动到Base模板类派生的另一个基类中可以解决这个问题。

如π α ντα ρ ε ε ε的答案所述,CRTP是这里的解。但是更正一下:方法thisMustBeDefined实际上并不需要Derived实现它。

int thisMustBeDefined() {
    // Incorrect theory: Will fail to compile if not declared in Der
    static_cast<Der*>(this)->thisMustBeDefined();
}

参见godbolt https://godbolt.org/z/x6o1T6jn1

可以使用已删除函数来要求在派生类中定义函数,但请注意,只有在程序中使用该函数时才有效。

template <typename Der>
class Base {
  public:
    int thisMustBeDefined() = delete; // Derived class must define this function
};
class Derived : public Base<Derived> {
  public:
    // int thisMustBeDefined() { return 11; } // LINE A
};
int main() {
    Derived d;
    d.thisMustBeDefined(); // LINE B
}
// Compile error will occur if (LINE A does not exist) && (LINE B exists)

参见godbolt https://godbolt.org/z/oz9ooe7Yc