为什么在链接阶段我会遇到与经过类型擦除的对象相关的虚表链接错误?
我正在为一个项目实现一个类型擦除(type-erased)的类。下面是一个我想要实现的简化示例:
#include <iostream>
struct MyClass
{
struct TypeWrapper
{
virtual void print_value() const;
};
template<typename ErasedType>
struct ErasedTypeClass: public TypeWrapper
{
ErasedType object;
ErasedTypeClass(ErasedType init_val): object(init_val) {}
void print_value() const override
{
std::cout << object << '\n';
}
};
TypeWrapper wrapper;
};
int main()
{
MyClass class_object_int;
class_object_int.wrapper = MyClass::ErasedTypeClass<int>(5);
MyClass class_object_float;
class_object_float.wrapper = MyClass::ErasedTypeClass<float>(10.0f);
}
我用 g++ -std=c++17 编译这个。MyClass 是一个容器,里面可以放各种类型的对象。这些对象只需要能够对它们调用 std::cout。在 main 我创建这个类的一个对象并在其中插入一个 int。然后我创建同一类型的另一个对象,并在其中插入一个 float。这段代码编译时没有任何问题。
然而,当链接器尝试链接这段代码时,抛出了以下错误:
/cefs/ee/eed41a3e34266cd09176ffa5_gcc-trunk-20260515/bin/../lib/gcc/x86_64-linux-gnu/17.0.0/../../../../x86_64-linux-gnu/bin/ld: /tmp/cclA5P5w.o: in function `main':
<source>:27:(.text+0x9): undefined reference to `vtable for MyClass::TypeWrapper'
/cefs/ee/eed41a3e34266cd09176ffa5_gcc-trunk-20260515/bin/../lib/gcc/x86_64-linux-gnu/17.0.0/../../../../x86_64-linux-gnu/bin/ld: <source>:30:(.text+0x36): undefined reference to `vtable for MyClass::TypeWrapper'
/cefs/ee/eed41a3e34266cd09176ffa5_gcc-trunk-20260515/bin/../lib/gcc/x86_64-linux-gnu/17.0.0/../../../../x86_64-linux-gnu/bin/ld: /tmp/cclA5P5w.o: in function `MyClass::TypeWrapper::TypeWrapper()':
<source>:5:(.text._ZN7MyClass11TypeWrapperC2Ev[_ZN7MyClass11TypeWrapperC5Ev]+0x9): undefined reference to `vtable for MyClass::TypeWrapper'
/cefs/ee/eed41a3e34266cd09176ffa5_gcc-trunk-20260515/bin/../lib/gcc/x86_64-linux-gnu/17.0.0/../../../../x86_64-linux-gnu/bin/ld: /tmp/cclA5P5w.o:(.rodata._ZTIN7MyClass15ErasedTypeClassIfEE[_ZTIN7MyClass15ErasedTypeClassIfEE]+0x10): undefined reference to `typeinfo for MyClass::TypeWrapper'
/cefs/ee/eed41a3e34266cd09176ffa5_gcc-trunk-20260515/bin/../lib/gcc/x86_64-linux-gnu/17.0.0/../../../../x86_64-linux-gnu/bin/ld: /tmp/cclA5P5w.o:(.rodata._ZTIN7MyClass15ErasedTypeClassIiEE[_ZTIN7MyClass15ErasedTypeClassIiEE]+0x10): undefined reference to `typeinfo for MyClass::TypeWrapper'
collect2: error: ld returned 1 exit status
ASM generation compiler returned: 1
/cefs/ee/eed41a3e34266cd09176ffa5_gcc-trunk-20260515/bin/../lib/gcc/x86_64-linux-gnu/17.0.0/../../../../x86_64-linux-gnu/bin/ld: /tmp/cc8zjuKh.o: in function `main':
<source>:27:(.text+0x9): undefined reference to `vtable for MyClass::TypeWrapper'
/cefs/ee/eed41a3e34266cd09176ffa5_gcc-trunk-20260515/bin/../lib/gcc/x86_64-linux-gnu/17.0.0/../../../../x86_64-linux-gnu/bin/ld: <source>:30:(.text+0x36): undefined reference to `vtable for MyClass::TypeWrapper'
/cefs/ee/eed41a3e34266cd09176ffa5_gcc-trunk-20260515/bin/../lib/gcc/x86_64-linux-gnu/17.0.0/../../../../x86_64-linux-gnu/bin/ld: /tmp/cc8zjuKh.o: in function `MyClass::TypeWrapper::TypeWrapper()':
<source>:5:(.text._ZN7MyClass11TypeWrapperC2Ev[_ZN7MyClass11TypeWrapperC5Ev]+0x9): undefined reference to `vtable for MyClass::TypeWrapper'
/cefs/ee/eed41a3e34266cd09176ffa5_gcc-trunk-20260515/bin/../lib/gcc/x86_64-linux-gnu/17.0.0/../../../../x86_64-linux-gnu/bin/ld: /tmp/cc8zjuKh.o:(.rodata._ZTIN7MyClass15ErasedTypeClassIfEE[_ZTIN7MyClass15ErasedTypeClassIfEE]+0x10): undefined reference to `typeinfo for MyClass::TypeWrapper'
/cefs/ee/eed41a3e34266cd09176ffa5_gcc-trunk-20260515/bin/../lib/gcc/x86_64-linux-gnu/17.0.0/../../../../x86_64-linux-gnu/bin/ld: /tmp/cc8zjuKh.o:(.rodata._ZTIN7MyClass15ErasedTypeClassIiEE[_ZTIN7MyClass15ErasedTypeClassIiEE]+0x10): undefined reference to `typeinfo for MyClass::TypeWrapper'
collect2: error: ld returned 1 exit status
Execution build compiler returned: 1
Build failed
以下是一个 compiler explorer 链接,你可以在那里看到实际情况。为了确保我的实现没有搞错,我还把代码粘贴到了 CppInsights 但即便如此我仍然没有看到任何问题。下面是CppInsights生成的内容:
#include <iostream>
struct MyClass
{
struct TypeWrapper
{
virtual void print_value() const;
// inline constexpr TypeWrapper & operator=(TypeWrapper &&) noexcept = default;
// inline constexpr TypeWrapper() noexcept = default;
};
template<typename ErasedType>
struct ErasedTypeClass : public TypeWrapper
{
ErasedType object;
inline ErasedTypeClass(ErasedType init_val)
: object(init_val)
{
}
inline virtual void print_value() const
{
(std::cout << this->object) << '\n';
}
};
/* First instantiated from: insights.cpp:28 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
struct ErasedTypeClass<int> : public TypeWrapper
{
int object;
inline ErasedTypeClass(int init_val)
: TypeWrapper()
, object{init_val}
{
}
inline virtual void print_value() const
{
std::operator<<(std::cout.operator<<(this->object), '\n');
}
};
#endif
/* First instantiated from: insights.cpp:31 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
struct ErasedTypeClass<float> : public TypeWrapper
{
float object;
inline ErasedTypeClass(float init_val)
: TypeWrapper()
, object{init_val}
{
}
inline virtual void print_value() const
{
std::operator<<(std::cout.operator<<(this->object), '\n');
}
};
#endif
TypeWrapper wrapper;
// inline constexpr MyClass() noexcept = default;
};
int main()
{
MyClass class_object_int = MyClass();
class_object_int.wrapper.operator=(static_cast<MyClass::TypeWrapper &&>(MyClass::ErasedTypeClass<int>(5)));
MyClass class_object_float = MyClass();
class_object_float.wrapper.operator=(static_cast<MyClass::TypeWrapper &&>(MyClass::ErasedTypeClass<float>(5.0F)));
return 0;
}
错误信息 undefined reference to 'vtable for MyClass::TypeWrapper' 究竟是什么意思,我该如何修复呢?
解决方案
多亏了 @VladfromMoscow和 @igor-tandetnik的评论,我找到了问题的解决办法。问题在于 MyClass::TypeWrapper 中的函数声明 virtual void print_value() const; 是一个声明,而不是一个定义。因此要修复它,我要么在 TypeWrapper 中提供一个定义,要么将其标记为纯虚函数。由于在我的情况下我找不到需要一个回退定义的理由,我将其标记为纯虚函数,以得到:
#include <iostream>
struct MyClass
{
struct TypeWrapper
{
virtual void print_value() const = 0;
};
template<typename ErasedType>
struct ErasedTypeClass: public TypeWrapper
{
ErasedType object;
ErasedTypeClass(ErasedType init_val): object(init_val) {}
void print_value() const override
{
std::cout << object << '\n';
}
};
TypeWrapper wrapper;
};
int main()
{
MyClass class_object_int;
class_object_int.wrapper = MyClass::ErasedTypeClass<int>(5);
MyClass class_object_float;
class_object_float.wrapper = MyClass::ErasedTypeClass<float>(10.0f);
}
然而,这段代码仍然无法编译。原因在于因为 TypeWrapper 不是一个抽象类,所以我不能再创建类型为 TypeWrapper 的对象。因此我要么把对象 wrapper 的类型改为 TypeWrapper*,要么改为 TypeWrapper&。我选择了使用引用的方式来实现:
#include <iostream>
struct MyClass
{
struct TypeWrapper
{
virtual void print_value() const = 0;
};
template<typename ErasedType>
struct ErasedTypeClass: public TypeWrapper
{
ErasedType object;
ErasedTypeClass(ErasedType init_val): object(init_val) {}
void print_value() const override
{
std::cout << object << '\n';
}
};
TypeWrapper& wrapper;
MyClass(TypeWrapper&& wrapper_): wrapper(wrapper_) {}
};
int main()
{
MyClass class_object_int(MyClass::ErasedTypeClass<int>(5));
MyClass class_object_float(MyClass::ErasedTypeClass<float>(10.0f));
}
这段代码可以编译,并且恰好实现了我想要的效果。