c++polymorphismvirtualabstractvtable

C++ polymorphism not working with ESP-IDF


I have an abstract class

namespace AComp
{
   class A 
    { 
    public:
      virtual void func() = 0;
      virtual ~A();
    };
    A::~A() { }
}

I also have an abstract sub-class which does not provide implementation for the pure virtual member

namespace BComp
{
    class B : public AComp::A
    {
    public:
      virtual void func() override = 0;
      virtual ~B() override;
    };
    B::~B() { }
}

Then I have a subclass providing implementation

namespace CComp
{
    class C : public BComp::B
    {
    public:
      virtual void func() override;
    };

    void C::func() { }
}

Finally, in my class which uses class C I have

AComp::A * instanceOfA = new CComp::C();

The linker throws the following error

undefined reference to 'vtable for B'

I thought it was something really stupid, but I cannot figure out what the issue is.

This code is to run on an ESP-32 microprocessor and it's all written using VisualCode and the ESP-IDF framework, where: A is in component Acomp, B is in component Bcomp, C is in component Ccomp and the calling code is in the main App

I've run the code through several online compilers and there appears to be no issues, however I do get the linker error

Compiler info

-- The C compiler identification is GNU 11.2.0
-- The CXX compiler identification is GNU 11.2.0
-- The ASM compiler identification is GNU
-- Found assembler: A:/espressif/.espressif/tools/xtensa-esp32-elf/esp-2022r1-11.2.0/xtensa-esp32-elf/bin/xtensa-esp32-elf-gcc.exe
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: A:/espressif/.espressif/tools/xtensa-esp32-elf/esp-2022r1-11.2.0/xtensa-esp32-elf/bin/xtensa-esp32-elf-gcc.exe - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: A:/espressif/.espressif/tools/xtensa-esp32-elf/esp-2022r1-11.2.0/xtensa-esp32-elf/bin/xtensa-esp32-elf-g++.exe - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Building ESP-IDF components for target esp32

Solution

  • @BenVoigt found the issue.

    The proximate cause is "the compiler didn't generate the vtable because it thought someone else (compiler running on another file) was going to do it". There's half a dozen things influencing that behavior, so it's hard to label any one of them as "the root cause". Do double-check that every single other virtual function in B has a definition and the definition is getting linked in. The linked explanation (stackoverflow.com/a/26928723/103167) suggests the problem is either (a) the first (in source code order) declared virtual functions or (b) a virtual function inherited from A that isn't pure in B.

    I placed the code as is in a new project and it compiled and linked without error. This lead me to go back to the code in the project.

    There were extra pertinent additions to both BComp::B and CComp::C.

    namespace BComp
    {
        class B : public AComp::A
        {
        protected:
         virtual void func1();
         virtual void func2();
        public:
          virtual void func() override = 0;
          virtual ~B() override;
        };
        B::~B() { }
    }
    
    namespace CComp
    {
        class C : public BComp::B
        {
        public:
          virtual void func() override;
        };
    
        void C::func() { }
        void C::func1() { }
        void C::func2() { }
    }
    

    BComp also declares some other non-pure virtual members, implemented in the sub-class CComp::C. To C#, VB.Net, Java, etc. users, this would appear natural; however, as @BenVoigt pointed out:

    If you declare a virtual member function, you have to either provide a definition in the same class or make it pure and provide a definition in a sub-class. Definition in sub-class is not good enough by itself.

    So the solution was to adhere to the rules of C++ programming, and had NOTHING to do with the target (ESP32), the compiler, or the linker.

    The solution is to make the virtual members of BComp::B pure:

    namespace BComp
    {
        class B : public AComp::A
        {
        protected:
         virtual void func1() = 0;
         virtual void func2() = 0;
        public:
          virtual void func() override = 0;
          virtual ~B() override;
        };
        B::~B() { }
    }
    

    Summary from @BenVoigt:

    Normally the linker would have given you unresolved external errors for the virtual functions that didn't have definitions in B. But the error message would have been "unresolved B::bar() referenced in vtable of B". Since you were also missing the vtable, none of those more meaningful errors were seen. Because the toolchain was dumb and generated an error for something the programmer is never supposed to see, instead of a meaningful message about the real problem [...]. [...] So now that you know the important parts (A isn't, func() isn't, the virtual function declared in B and implemented in C is).