当构建我的c++程序时,我得到了错误消息

未定义的引用'vtable…

这个问题的原因是什么?我该怎么解决呢?


碰巧,我得到以下代码的错误(类的问题是CGameModule.),我不能为我的生活理解问题是什么。起初,我认为这与忘记给虚拟函数赋予一个主体有关,但据我所知,一切都在这里。继承链有点长,但这里有相关的源代码。我不确定我还需要提供什么信息。

注意:这个错误似乎发生在构造函数中。

我的代码:

class CGameModule : public CDasherModule {
 public:
  CGameModule(Dasher::CEventHandler *pEventHandler, CSettingsStore *pSettingsStore, CDasherInterfaceBase *pInterface, ModuleID_t iID, const char *szName)
  : CDasherModule(pEventHandler, pSettingsStore, iID, 0, szName)
  { 
      g_pLogger->Log("Inside game module constructor");   
      m_pInterface = pInterface; 
  }

  virtual ~CGameModule() {};

  std::string GetTypedTarget();

  std::string GetUntypedTarget();

  bool DecorateView(CDasherView *pView) {
      //g_pLogger->Log("Decorating the view");
      return false;
  }

  void SetDasherModel(CDasherModel *pModel) { m_pModel = pModel; }


  virtual void HandleEvent(Dasher::CEvent *pEvent); 

 private:



  CDasherNode *pLastTypedNode;


  CDasherNode *pNextTargetNode;


  std::string m_sTargetString;


  size_t m_stCurrentStringPos;


  CDasherModel *m_pModel;


  CDasherInterfaceBase *m_pInterface;
};

继承自…

class CDasherModule;
typedef std::vector<CDasherModule*>::size_type ModuleID_t;

/// \ingroup Core
/// @{
class CDasherModule : public Dasher::CDasherComponent {
 public:
  CDasherModule(Dasher::CEventHandler * pEventHandler, CSettingsStore * pSettingsStore, ModuleID_t iID, int iType, const char *szName);

  virtual ModuleID_t GetID();
  virtual void SetID(ModuleID_t);
  virtual int GetType();
  virtual const char *GetName();

  virtual bool GetSettings(SModuleSettings **pSettings, int *iCount) {
    return false;
  };

 private:
  ModuleID_t m_iID;
  int m_iType;
  const char *m_szName;
};

哪个继承自....

namespace Dasher {
  class CEvent;
  class CEventHandler;
  class CDasherComponent;
};

/// \ingroup Core
/// @{
class Dasher::CDasherComponent {
 public:
  CDasherComponent(Dasher::CEventHandler* pEventHandler, CSettingsStore* pSettingsStore);
  virtual ~CDasherComponent();

  void InsertEvent(Dasher::CEvent * pEvent);
  virtual void HandleEvent(Dasher::CEvent * pEvent) {};

  bool GetBoolParameter(int iParameter) const;
  void SetBoolParameter(int iParameter, bool bValue) const;

  long GetLongParameter(int iParameter) const;
  void SetLongParameter(int iParameter, long lValue) const;

  std::string GetStringParameter(int iParameter) const;
  void        SetStringParameter(int iParameter, const std::string & sValue) const;

  ParameterType   GetParameterType(int iParameter) const;
  std::string     GetParameterName(int iParameter) const;

 protected:
  Dasher::CEventHandler *m_pEventHandler;
  CSettingsStore *m_pSettingsStore;
};
/// @}


#endif

当前回答

这是GCC中的一个错误特性。也就是说,g++编译器本身不能抱怨未定义的虚方法,因为它们可以在其他地方定义。但是-它不存储关于缺少哪些虚拟成员的信息;它只存储了一个und定义的虚表符号,链接器会抱怨。

相反,如果要列出缺少的成员,则链接器可以告诉您它们是什么。

关于这个问题,GCC有一个公开的bug: bug 42540。不幸的是,它已经13岁了。

其他回答

这里有很多猜测,各种各样的答案。下面我将给出一个相当小的代码来重现这个错误,并解释为什么会出现这个错误。

重现此错误的代码相当少

IBase.hpp

#pragma once

class IBase {
    public:
        virtual void action() = 0;
};

Derived.hpp

#pragma once

#include "IBase.hpp"

class Derived : public IBase {
    public:
        Derived(int a);
        void action() override;
};

Derived.cpp

#include "Derived.hpp"
Derived::Derived(int a) { }
void Derived::action() {}

myclass.cpp

#include <memory>
#include "Derived.hpp"

class MyClass {

    public:
        MyClass(std::shared_ptr<Derived> newInstance) : instance(newInstance) {

        }

        void doSomething() {
            instance->action();
        }

    private:
        std::shared_ptr<Derived> instance;
};

int main(int argc, char** argv) {
    Derived myInstance(5);
    MyClass c(std::make_shared<Derived>(myInstance));
    c.doSomething();
    return 0;
}

你可以像这样使用GCC编译:

g++ -std=c++11 -o a.out myclass.cpp Derived.cpp

现在可以通过在hbase .hpp中删除= 0来重现错误。我得到这个错误:

~/.../catkin_ws$ g++ -std=c++11 -o /tmp/m.out /tmp/myclass.cpp /tmp/Derived.cpp
/tmp/cclLscB9.o: In function `IBase::IBase(IBase const&)':
myclass.cpp:(.text._ZN5IBaseC2ERKS_[_ZN5IBaseC5ERKS_]+0x13): undefined reference to `vtable for IBase'
/tmp/cc8Smvhm.o: In function `IBase::IBase()':
Derived.cpp:(.text._ZN5IBaseC2Ev[_ZN5IBaseC5Ev]+0xf): undefined reference to `vtable for IBase'
/tmp/cc8Smvhm.o:(.rodata._ZTI7Derived[_ZTI7Derived]+0x10): undefined reference to `typeinfo for IBase'
collect2: error: ld returned 1 exit status

解释

请注意,上面的代码不需要任何虚拟析构函数、构造函数或任何其他额外的文件才能成功编译(尽管您应该拥有它们)。

The way to understand this error is as follows: Linker is looking for constructor of IBase. This it will need it for the constructor of Derived. However as Derived overrides methods from IBase, it has vtable attached to it that will reference IBase. When linker says "undefined reference to vtable for IBase" it basically means that Derived has vtable reference to IBase but it can't find any compiled object code of IBase to look up to. So the bottom line is that class IBase has declarations without implementations. This means a method in IBase is declared as virtual but we forgot to mark it as pure virtual OR provide its definition.

分小费

如果所有这些都失败了,那么调试这个错误的一种方法是构建最小的程序,它可以编译,然后不断更改它,使它达到您想要的状态。在此期间,继续编译以查看它何时开始失败。

注意ROS和柳絮构建系统

如果你在ROS中使用catkin构建系统编译上述一组类,那么你将需要在CMakeLists.txt中执行以下命令:

add_executable(myclass src/myclass.cpp src/Derived.cpp)
add_dependencies(myclass theseus_myclass_cpp)
target_link_libraries(myclass ${catkin_LIBRARIES})

第一行基本上是说,我们想要创建一个名为myclass的可执行文件,构建它的代码可以在下面的文件中找到。其中一个文件应该有main()。请注意,您不必在CMakeLists.txt中的任何地方指定.hpp文件。此外,您不必将Derived.cpp指定为库。

解释为什么虚表可能丢失,以及如何修复它。答案很长,因为它解释了为什么编译器可能会忘记创建虚表。(编辑)

什么是虚表?

在尝试修复错误消息之前,了解错误消息所谈论的内容可能是有用的。我将从高水平开始,然后再深入到更详细的细节。这样,一旦人们熟悉了虚变量,就可以跳过前面的内容。现在有一群人在前面跳。对于那些留下来的人:

A vtable is basically the most common implementation of polymorphism in C++. When vtables are used, every polymorphic class has a vtable somewhere in the program; you can think of it as a (hidden) static data member of the class. Every object of a polymorphic class is associated with the vtable for its most-derived class. By checking this association, the program can work its polymorphic magic. Important caveat: a vtable is an implementation detail. It is not mandated by the C++ standard, even though most (all?) C++ compilers use vtables to implement polymorphic behavior. The details I am presenting are either typical or reasonable approaches. Compilers are allowed to deviate from this!

每个多态对象都有一个(隐藏的)指向对象最派生类虚表的指针(在更复杂的情况下,可能有多个指针)。通过查看指针,程序可以判断对象的“真实”类型是什么(除了在构造过程中,但让我们跳过这种特殊情况)。例如,如果类型A的对象不指向A的虚表,那么该对象实际上是派生自A的某个对象的子对象。

The name "vtable" comes from "virtual function table". It is a table that stores pointers to (virtual) functions. A compiler chooses its convention for how the table is laid out; a simple approach is to go through the virtual functions in the order they are declared within class definitions. When a virtual function is called, the program follows the object's pointer to a vtable, goes to the entry associated with the desired function, then uses the stored function pointer to invoke the correct function. There are various tricks for making this work, but I won't go into those here.

在哪里/何时生成虚表?

A vtable is automatically generated (sometimes called "emitted") by the compiler. A compiler could emit a vtable in every translation unit that sees a polymorphic class definition, but that would usually be unnecessary overkill. An alternative (used by gcc, and probably by others) is to pick a single translation unit in which to place the vtable, similar to how you would pick a single source file in which to put a class' static data members. If this selection process fails to pick any translation units, then the vtable becomes an undefined reference. Hence the error, whose message is admittedly not particularly clear.

类似地,如果选择过程确实选择了一个翻译单元,但是该目标文件没有提供给链接器,那么虚表将成为一个未定义的引用。不幸的是,在这种情况下,错误消息甚至比选择过程失败的情况更不清楚。(感谢那些提到这种可能性的受访者。否则我可能已经忘记了。)

The selection process used by gcc makes sense if we start with the tradition of devoting a (single) source file to each class that needs one for its implementation. It would be nice to emit the vtable when compiling that source file. Let's call that our goal. However, the selection process needs to work even if this tradition is not followed. So instead of looking for the implementation of the entire class, let's look for the implementation of a specific member of the class. If tradition is followed – and if that member is in fact implemented – then this achieves the goal.

The member selected by gcc (and potentially by other compilers) is the first non-inline virtual function that is not pure virtual. If you are part of the crowd that declares constructors and destructors before other member functions, then that destructor has a good chance of being selected. (You did remember to make the destructor virtual, right?) There are exceptions; I'd expect that the most common exceptions are when an inline definition is provided for the destructor and when the default destructor is requested (using "= default").

精明的人可能会注意到,允许多态类为其所有虚函数提供内联定义。这难道不会导致选择过程失败吗?在旧的编译器中是这样的。我听说最新的编译器已经解决了这个问题,但是我不知道相关的版本号。我可以尝试查找这个,但它更容易要么编码绕过它或等待编译器抱怨。

总之,有三个关键原因导致“未定义的引用虚表”错误:

成员函数缺少定义。 目标文件未被链接。 所有虚函数都有内联定义。

这些原因本身不足以导致错误。相反,这些是解决错误的方法。不要期望故意创建这些情况之一一定会产生此错误;还有其他要求。请期望解决这些情况将解决此错误。

(好吧,当这个问题被问到时,第3条可能已经足够了。)

如何修复错误?

欢迎回来,跳过前面的人!:)

Look at your class definition. Find the first non-inline virtual function that is not pure virtual (not "= 0") and whose definition you provide (not "= default"). If there is no such function, try modifying your class so there is one. (Error possibly resolved.) See also the answer by Philip Thomas for a caveat. Find the definition for that function. If it is missing, add it! (Error possibly resolved.) If the function definition is outside the class definition, then make sure the function definition uses a qualified name, as in ClassName::function_name. Check your link command. If it does not mention the object file with that function's definition, fix that! (Error possibly resolved.) Repeat steps 2 and 3 for each virtual function, then for each non-virtual function, until the error is resolved. If you're still stuck, repeat for each static data member.

例子 具体操作的细节可能会有所不同,有时还会分成不同的问题(比如什么是未定义的引用/未解决的外部符号错误以及如何修复它?)不过,我将提供一个在特定情况下该如何做的示例,这可能会使新程序员感到困惑。

步骤1提到修改类,使其具有特定类型的函数。如果这个功能的描述超出了你的理解范围,那么你可能正处于我想要解决的情况。记住,这是实现目标的一种方式;这不是唯一的方法,在你的具体情况下,很容易有更好的方法。让我们称你的类为a。你的析构函数(在你的类定义中)声明为两者之一

virtual ~A() = default;

or

virtual ~A() {}

? 如果是这样,两个步骤将把析构函数更改为我们想要的函数类型。首先,将该行更改为

virtual ~A();

其次,在项目的源文件中放入以下一行(最好是类实现的文件,如果你有的话):

A::~A() {}

这使得你的(虚拟)析构函数是非内联的,并且不是由编译器生成的。(请随意修改内容以更好地匹配您的代码格式风格,例如在函数定义中添加头注释。)

我刚刚遇到了另一个导致这个错误的原因,你可以检查一下。

基类将纯虚函数定义为:

virtual int foo(int x = 0);

子类有

int foo(int x) override;

问题是“=0”应该在括号外的拼写错误:

virtual int foo(int x) = 0;

所以,如果你向下滚动这么远,你可能没有找到答案,这是另一个需要检查的东西。

你确定CDasherComponent有一个析构函数体吗?它肯定不在这里——问题是它是否在.cc文件中。 从样式的角度来看,CDasherModule应该显式地定义它的析构函数虚函数。 看起来CGameModule在末尾有一个额外的}(在}之后;//类)。 CGameModule是否被链接到定义CDasherModule和CDasherComponent的库中?

我尝试了JaMIT的所有详细步骤,仍然被这个错误难住了。经过一番反复,我终于明白了。我太粗心了。您应该能够使用以下示例代码重现这个令人痛苦的错误。

[jaswantp@jaswant-arch build]$ gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0/lto-wrapper
Target: x86_64-pc-linux-gnu
Configured with: /build/gcc/src/gcc/configure --prefix=/usr --libdir=/usr/lib --libexecdir=/usr/lib --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=https://bugs.archlinux.org/ --enable-languages=c,c++,ada,fortran,go,lto,objc,obj-c++,d --with-isl --with-linker-hash-style=gnu --with-system-zlib --enable-__cxa_atexit --enable-cet=auto --enable-checking=release --enable-clocale=gnu --enable-default-pie --enable-default-ssp --enable-gnu-indirect-function --enable-gnu-unique-object --enable-install-libiberty --enable-linker-build-id --enable-lto --enable-multilib --enable-plugin --enable-shared --enable-threads=posix --disable-libssp --disable-libstdcxx-pch --disable-libunwind-exceptions --disable-werror gdc_include_dir=/usr/include/dlang/gdc
Thread model: posix
Supported LTO compression algorithms: zlib zstd
gcc version 10.2.0 (GCC) 

// CelesetialBody.h
class CelestialBody{
public:
    virtual void Print();
protected:
    CelestialBody();
    virtual ~CelestialBody();
};
// CelestialBody.cpp
#include "CelestialBody.h"

CelestialBody::CelestialBody() {}

CelestialBody::~CelestialBody() = default;

void CelestialBody::Print() {}
// Planet.h
#include "CelestialBody.h"

class Planet : public CelestialBody
{
public:
    void Print() override;
protected:
    Planet();
    ~Planet() override;
};
// Planet.cpp
#include "Planet.h"

Planet::Planet() {}
Planet::~Planet() {}

void Print() {} // Deliberately forgot to prefix `Planet::`
# CMakeLists.txt
cmake_minimum_required(VERSION 3.12)
project (space_engine)
add_library (CelestialBody SHARED CelestialBody.cpp)
add_library (Planet SHARED Planet.cpp)
target_include_directories (CelestialBody PRIVATE ${CMAKE_CURRENT_LIST_DIR})  
target_include_directories (Planet PRIVATE ${CMAKE_CURRENT_LIST_DIR})    
target_link_libraries (Planet PUBLIC CelestialBody)

# hardened linker flags to catch undefined symbols
target_link_options(Planet 
    PRIVATE 
    -Wl,--as-needed
    -Wl,--no-undefined
)

我们得到了我们最喜欢的误差。

$ mkdir build
$ cd build
$ cmake ..
$ make
[ 50%] Built target CelestialBody
Scanning dependencies of target Planet
[ 75%] Building CXX object CMakeFiles/Planet.dir/Planet.cpp.o
[100%] Linking CXX shared library libPlanet.so
/usr/bin/ld: CMakeFiles/Planet.dir/Planet.cpp.o: in function `Planet::Planet()':
Planet.cpp:(.text+0x1b): undefined reference to `vtable for Planet'
/usr/bin/ld: CMakeFiles/Planet.dir/Planet.cpp.o: in function `Planet::~Planet()':
Planet.cpp:(.text+0x3d): undefined reference to `vtable for Planet'
collect2: error: ld returned 1 exit status
make[2]: *** [CMakeFiles/Planet.dir/build.make:104: libPlanet.so] Error 1
make[1]: *** [CMakeFiles/Makefile2:97: CMakeFiles/Planet.dir/all] Error 2
make: *** [Makefile:103: all] Error 2

我在Planet.cpp中所做的事情当然应该通过这个技巧来解决

查看类定义。找到第一个非纯虚函数(不是“= 0”),并且您提供了其定义(不是“= default”)的非内联虚函数。

JaMIT的回答。 如果有其他人尝试了以上所有方法,但没有任何效果,可能您也像我一样,不小心忘记给一个或多个成员函数加上<ClassName>::前缀。

要么我需要检查一下眼睛,要么我需要睡一会儿。