-1

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
  • 5
    [Can't reproduce](https://godbolt.org/z/Yss7xjGGG) - Please provide a [mre]. Also, `A` should have a `virtual` destructor. – Ted Lyngmo Aug 30 '23 at 17:09
  • 3
    You going to find that pretty much all errors are dumb errors when you figure them out. – user4581301 Aug 30 '23 at 17:10
  • 2
    A really dumb [question title](https://meta.stackexchange.com/a/388112/620615) too. – user3840170 Aug 30 '23 at 17:35
  • 1
    This is a real problem and you aren't dumb for having it. However, you will need to provide details of which compiler and target architecture you are using. Ok, ESP32 is the target architecture. But the name of a text editor is not details on the compiler. – Ben Voigt Aug 30 '23 at 18:27
  • 1
    Different platform but same problem: https://stackoverflow.com/a/26928723/103167 – Ben Voigt Aug 30 '23 at 18:30
  • @BenVoigt, sadly re-running of cmake does not resolve the problem. Also, I've included the compiler info in the question – Bill Richards Aug 30 '23 at 18:43
  • @user3840170, thanks for your constructive input – Bill Richards Aug 30 '23 at 18:43
  • Is it a [mcve]? What happens if you compile your code on your target machine as is? – n. m. could be an AI Aug 30 '23 at 18:48
  • @n.m.couldbeanAI: that is a very good question. I'm going to fire up a new project and include just these definitions ... perhaps there is more going on in the *actual code* than that which I have distilled it down to – Bill Richards Aug 30 '23 at 18:53
  • @BillRichards: Never meant to suggest that a rebuild will fix it for you. The salient points in the linked answer were the explanation of how the compiler and linker arrange to have exactly one copy of the v-table, by looking for the compilation unit which implements the first virtual function in the v-table. – Ben Voigt Aug 30 '23 at 19:04
  • Sorry @BenVoigt, the takeaway that I had from the linked issue was essentially "re-run cmake to get everything in the right place" – Bill Richards Aug 30 '23 at 19:08
  • The details on which is that "first virtual function" for Qt and why that makes this error appear when `moc` is forgotten for Qt classes do not directly apply to you -- you don't inherit Qt classes and don't use `moc`. – Ben Voigt Aug 30 '23 at 19:08
  • 1
    Nope, this is a tricky one. You can't skip over the "understanding it" phase straight to "what worked for someone else". – Ben Voigt Aug 30 '23 at 19:09
  • @BillRichards Not entirely sure if this was sarcastic or not, but either way, you’re welcome! – user3840170 Aug 30 '23 at 19:19
  • @user3840170 no sarcasm intended :o) – Bill Richards Aug 30 '23 at 19:27
  • @BenVoigt as indicated, I've created a new project containing solely the example code given in the question. That compiles and links fine. So, I'm now going to have to make sure I *_fully_* understand the linked response, and figure out how to make sure the vtable definition ends up in the right TU (I think) – Bill Richards Aug 30 '23 at 19:40
  • If you remove `func()` from `B` entirely (it'll still be inherited pure virtual) does that help at all? – Ben Voigt Aug 30 '23 at 20:26
  • Related (but not nearly as in-depth an analysis as my earlier link): https://stackoverflow.com/a/12574407/103167 – Ben Voigt Aug 30 '23 at 20:27
  • @BenVoigt removing `func()` declaration from `B` has no effect, though I have just realized that `B` also contains other `virtual` members, maybe this is the root cause? – Bill Richards Aug 30 '23 at 20:37
  • 2
    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 (much earlier) explanation 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. – Ben Voigt Aug 30 '23 at 20:42
  • @BenVoigt All virtual functions declared in `B` are defined in `C` along with `func()` , however none of the virtual functions declared in `B` are pure – Bill Richards Aug 30 '23 at 20:46
  • @BenVoigt ... After removing `A` (and all references to the pure virtual func()) from the inheritance chain I am getting the same error from the linker; so it seems there is something wrong with my implementation of `B` ... like I initially said, I think it's going to be some dumb user error – Bill Richards Aug 30 '23 at 21:05
  • 2
    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. – Ben Voigt Aug 30 '23 at 21:37
  • @BenVoigt ... once I made the virtual members of `B` pure virtual, the problem is solved. I've been away from C++ for too long. Thank you for all of your help – Bill Richards Aug 30 '23 at 21:37
  • 1
    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. – Ben Voigt Aug 30 '23 at 21:38
  • 1
    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, it took you a whole day to solve. That makes for a good StackOverflow question. 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), you can write a much better question and answer, that will help the next person solve this error in one-tenth the time. – Ben Voigt Aug 30 '23 at 21:44

1 Answers1

1

I'm providing the answer to my own question so that anyone coming across the same issue won't have to read through the comments on the OP to find the answer.

As it turned out, it was indeed a case of user error, but it was not as "dumb" as I originally thought -but it was still something that a full-time C++ programmer would have most likely not encountered.

My caveat is that I have not used C++ "in anger" for a number of years and rather I have been working with higher level languages -mostly C#, with the occasional dip into Java

I have to credit @BenVoigt for his dilligence and persistence in finding the actual 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 also have to credit both @n.m.couldbeanAI and @Ted Lyngmo for insisting upon a minimal reproducible example

I placed the code as is from the OP in a new project, and unsurprisingly everything was fine, i.e. the code compiled and linked without error. This lead me to go back to the actual code in the project I was working on which had given me cause to pose the question here. There was clearly something else going on within that codebase that I had not represented in the OP.

As it turned out, there were some extra (and pertinent) features in my real codebase. The additions were to both BComp::B and CComp::C. The true representation therefore became

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() { }
}

As you can see, BComp also declares some other non-pure virtual members, which are implememnted in the sub-class CComp::C. Now, to any 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.

And so, the solution to the problem -which took a whole day of investigation by an experienced software engineer (that would be me!)- was in fact to adhere to the rules of C++ programming, and had NOTHING to do with the target (ESP32), the compiler, or the linker.

And here is the solution: 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() { }
}

Thanks again to all who contributed their time and effort. I will leave you with this summary (once again 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, it took you a whole day to solve. That makes for a good StackOverflow question. 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).