Alternative schemes for implementing vptr?

前端 未结 4 764
北海茫月
北海茫月 2021-01-18 02:46

This question is not about the C++ language itself(ie not about the Standard) but about how to call a compiler to implement alternative schemes for virtual function.

4条回答
  •  心在旅途
    2021-01-18 03:03

    No, there is no such switch.

    The LLVM/Clang codebase avoids virtual tables in classes that are allocated by the tens of thousands: this work well in a closed hierachy, because a single enum can enumerate all possible classes and then each class is linked to a value of the enum. The closed is obviously because of the enum.

    Then, virtuality is implemented by a switch on the enum, and appropriate casting before calling the method. Once again, closed. The switch has to be modified for each new class.


    A first alternative: external vpointer.

    If you find yourself in a situation where the vpointer tax is paid way too often, that is most of the objects are of known type. Then you can externalize it.

    class Interface {
    public:
      virtual ~Interface() {}
    
      virtual Interface* clone() const = 0; // might be worth it
    
      virtual void updateCount(int) = 0;
    
    protected:
      Interface(Interface const&) {}
      Interface& operator=(Interface const&) { return *this; }
    };
    
    template 
    class InterfaceBridge: public Interface {
    public:
      InterfaceBridge(T& t): t(t) {}
    
      virtual InterfaceBridge* clone() const { return new InterfaceBridge(*this); }
    
      virtual void updateCount(int i) { t.updateCount(i); }
    
    private:
      T& t; // value or reference ? Choose...
    };
    
    template 
    InterfaceBridge interface(T& t) { return InterfaceBridge(t); }
    

    Then, imagining a simple class:

    class Counter {
    public:
      int getCount() const { return c; }
      void updateCount(int i) { c = i; }
    private:
      int c;
    };
    

    You can store the objects in an array:

    static Counter array[5];
    
    assert(sizeof(array) == sizeof(int)*5); // no v-pointer
    

    And still use them with polymorphic functions:

    void five(Interface& i) { i.updateCount(5); }
    
    InterfaceBridge ib(array[3]); // create *one* v-pointer
    five(ib);
    
    assert(array[3].getCount() == 5);
    

    The value vs reference is actually a design tension. In general, if you need to clone you need to store by value, and you need to clone when you store by base class (boost::ptr_vector for example). It is possible to actually provide both interfaces (and bridges):

    Interface <--- ClonableInterface
      |                 |
    InterfaceB     ClonableInterfaceB
    

    It's just extra typing.


    Another solution, much more involved.

    A switch is implementable by a jump table. Such a table could perfectly be created at runtime, in a std::vector for example:

    class Base {
    public:
      ~Base() { VTables()[vpointer].dispose(*this); }
    
      void updateCount(int i) {
        VTables()[vpointer].updateCount(*this, i);
      }
    
    protected:
      struct VTable {
        typedef void (*Dispose)(Base&);
        typedef void (*UpdateCount)(Base&, int);
    
        Dispose dispose;
        UpdateCount updateCount;
      };
    
      static void NoDispose(Base&) {}
    
      static unsigned RegisterTable(VTable t) {
        std::vector& v = VTables();
        v.push_back(t);
        return v.size() - 1;
      }
    
      explicit Base(unsigned id): vpointer(id) {
        assert(id < VTables.size());
      }
    
    private:
      // Implement in .cpp or pay the cost of weak symbols.
      static std::vector VTables() { static std::vector VT; return VT; }
    
      unsigned vpointer;
    };
    

    And then, a Derived class:

    class Derived: public Base {
    public:
      Derived(): Base(GetID()) {}
    
    private:
      static void UpdateCount(Base& b, int i) {
        static_cast(b).count = i;
      }
    
      static unsigned GetID() {
        static unsigned ID = RegisterTable(VTable({&NoDispose, &UpdateCount}));
        return ID;
      }
    
      unsigned count;
    };
    

    Well, now you'll realize how great it is that the compiler does it for you, even at the cost of some overhead.

    Oh, and because of alignment, as soon as a Derived class introduces a pointer, there is a risk that 4 bytes of padding are used between Base and the next attribute. You can use them by careful selecting the first few attributes in Derived to avoid padding...

提交回复
热议问题