【问题标题】:Inlining causes specialized member function of template class overriding virtual functions to get overlooked内联导致模板类的特殊成员函数覆盖虚函数被忽略
【发布时间】:2013-01-10 18:32:44
【问题描述】:

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

为了让这个例子正常工作,你需要:

  • 三角形虚拟继承(在成员函数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();
}

现在,不要(!)为类型等于 bool 的 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&lt;bool&gt;::getAsString() 的调用中。另一方面,对于valbool,这不起作用,但模板被实例化并用作函数。然后链接器找到实例化的模板函数和第二个模块中给出的专用函数,并决定后者。

你怎么看?

  • 您认为这种行为是错误吗?
  • 为什么 Parameter&lt;bool&gt;::getAsString() 内联有效,而 Value&lt;bool&gt;::getAsString() 却无效,尽管两者都覆盖了虚函数?

【问题讨论】:

  • 请在每个代码块前加上它来自哪个文件。
  • 我没有涉足这段代码,但“在另一个模块中(这很重要)”几乎可以肯定是问题所在。模板特化必须在应该使用的地方可见。
  • 看起来您正在调用未定义的行为。那“但是在另一个模块中(这很重要)!”是您似乎正在这样做的地方。
  • 感谢您的观看。我按照建议做了序言。 (缺少的)前向声明肯定是现场。

标签: c++ templates inline specialization one-definition-rule


【解决方案1】:

我推测您遇到了 ODR 问题,因此猜测为什么某些编译器优化的行为与其他编译器设置不同是没有意义的。

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

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

因此,如果您很幸运,您会收到有关缺少声明/定义的编译器错误,但如果您真的很不幸,您会得到与您的预期不符的“工作”代码。

修复:始终包含(转发)所有模板特化的声明。最好将它们放在一个标头中,并包含来自所有调用您的类的所有客户端的标头以获取任何可能的模板参数。

// 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

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

【讨论】:

  • 我将此标记为正确答案。我主要想分享这个例子,因为我找不到任何与 stackoverflow 相关的东西,这是一个美丽的景象,可以找到你遇到的问题的解决方案。在我现实生活中的问题中,我可以强制实例化模板并在我想要的任何地方给出前向声明,编译器从不抱怨歧义或类似问题。这是我能想到的封闭式 MWE。
  • @FrederikHeber 在 Andrei Alexandrescu 的 Boost 邮件列表中,有一个旧的 post 关于这些问题,也与特征与策略有关。我认为它影响了当前出现在 C++11 标准库中的allocator_traits
  • 感谢分享非常有趣的帖子,它就在现场。 .. 及以上我的意思是写 closest MWE ...
猜你喜欢
  • 2014-08-16
  • 2015-03-04
  • 2014-03-08
  • 1970-01-01
  • 2021-11-08
  • 1970-01-01
  • 2017-06-28
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多