内嵌导致覆盖虚拟函数的模板类的专门成员函数被忽略

Inlining causes specialized member function of template class overriding virtual functions to get overlooked

本文关键字:函数 成员 覆盖 虚拟      更新时间:2023-10-16

我想和你们分享一个我偶然发现的奇怪的例子,这个例子让我思考了两天。

对于这个例子,你需要:

  • 三角形虚拟继承(关于成员函数getAsString()(
  • 覆盖虚拟函数的模板类(此处为Value<bool>::getAsString()(的成员函数专用化
  • 编译器的(自动(内联

您从一个模板类开始,该类实际上继承了一个公共接口,即一组虚拟函数。稍后,我们将专门化其中一个虚拟函数。嵌入可能会导致我们的特定化被忽视。

// test1.cpp and test2.cpp
#include <string>
class ValueInterface_common
{
public:
  virtual ~ValueInterface_common() {}
  virtual const std::string getAsString() const=0;
};
template <class T>
class Value :
  virtual public ValueInterface_common
{
public:
  virtual ~Value() {}
  const std::string getAsString() const;
};
template <class T>
inline const std::string Value<T>::getAsString() const
{
  return std::string("other type");
}   

接下来,我们必须继承这个Value类和Parameter类中本身也需要模板化的接口:

// test1.cpp
template <class T>
class Parameter :
  virtual public Value<T>,
  virtual public ValueInterface_common
{
public:
  virtual ~Parameter() {}
  const std::string getAsString() const;
};
template<typename T>
inline const std::string Parameter<T>::getAsString() const
{
  return Value<T>::getAsString();
}

现在,对于类型等于布尔的Value,不要(!(给出专门化的正向声明。。。

// NOT in: test1.cpp
template <>
const std::string Value<bool>::getAsString() const;

但相反,简单地给出这样的定义。。。

// test2.cpp
template <>
const std::string Value<bool>::getAsString() const
{
  return std::string("bool");
}

但在另一个模块中(这很重要(!

最后,我们有一个main()函数来测试正在发生的事情:

// test1.cpp
#include <iostream>
int main(int argc, char **argv)
{
  ValueInterface_common *paraminterface = new Parameter<bool>();
  Parameter<int> paramint;
  Value<int> valint;
  Value<bool> valbool;
  Parameter<bool> parambool;
  std::cout << "paramint is " << paramint.getAsString() << std::endl;
  std::cout << "parambool is " << parambool.getAsString() << std::endl;
  std::cout << "valint is " << valint.getAsString() << std::endl;
  std::cout << "valbool is " << valbool.getAsString() << std::endl;
  std::cout << "parambool as PI is " << paraminterface->getAsString() << std::endl;
  delete paraminterface;
  return 0;
}

如果您按照以下方式编译代码(我将其放入名为test1.cpp和test2.cpp的两个模块中,后者只包含专门化和必要的声明(:

g++ -O3 -g test1.cpp test2.cpp -o test && ./test

输出为

paramint is other type
parambool is other type
valint is other type
valbool is bool
parambool as PI is other type

如果您使用-O0或仅使用-fno-inline进行编译,或者如果您确实给出了专门化的前向声明,结果将变为:

paramint is other type
parambool is bool
valint is other type
valbool is bool
parambool as PI is bool

很有趣,不是吗?

到目前为止,我的解释是:内联在第一个模块(test.cpp(中起作用。所需的模板函数被实例化,但有些函数最终被内联在对Parameter<bool>::getAsString()的调用中。另一方面,对于valbool,这不起作用,但模板被实例化并用作函数。然后,链接器找到实例化的模板函数和第二个模块中给定的专用模板函数,并决定后者。

你觉得怎么样?

  • 你认为这种行为是个bug吗
  • 为什么内联对Parameter<bool>::getAsString()有效,但对Value<bool>::getAsString()无效,尽管两者都覆盖了一个虚拟函数

我推测您有一个ODR问题,所以猜测为什么某些编译器优化的行为与另一个编译器设置不同是没有意义的。

本质上,One Definition Rule规定同一实体应在整个应用程序中具有完全相同的定义,否则效果未定义

根本问题是,没有看到类模板成员函数的专用版本的代码可能仍在编译,可能会链接,有时甚至可能会运行。这是因为在没有显式专门化(的前向声明(的情况下,非专门化版本开始生效,很可能实现了适用于您的专门化类型的通用功能。

因此,如果你运气好的话,你会得到一个关于缺少声明/定义的编译器错误,但如果你真的运气不好,你会收到不符合你预期的"工作"代码。

修复方法:始终包含所有模板专门化的(前向(声明。最好将它们放在一个单独的头中,并包括来自所有客户端的头,这些客户端为任何可能的模板参数调用您的类。

// my_template.hpp
#include "my_template_fwd.hpp"
#include "my_template_primary.hpp"
#include "my_template_spec_some_type.hpp" 
// my_template_fwd.hpp
template<typename> class my_template; // forward declaration of the primary template
// my_template_primary.hpp
#include "my_template_fwd.hpp"
template<typename T> class my_template { /* full definition */ };
// my_template_spec_some_type.hpp
#include "my_template_fwd.hpp"
template<> class my_template<some_type> { /* full definition */ };
// some_client_module.hpp
#include "my_template.hpp" // no ODR possible, compiler will always see unique definition

显然,您可以通过为模板专门化创建子目录来重新组织命名,并相应地更改包含路径。