4

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

要使此示例正常工作,您需要:

  • 三角形虚继承(关于成员函数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类型等于 bool 的特化的前向声明......

// 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这不起作用,但模板被实例化并用作函数。然后链接器找到实例化的模板函数和第二个模块中给出的专用函数,并决定后者。

你怎么看呢?

  • 你认为这种行为是一个错误吗?
  • 为什么内联适用于Parameter<bool>::getAsString()但不适用于Value<bool>::getAsString()虽然两者都覆盖了虚函数?
4

1 回答 1

4

我推测你有一个 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

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

于 2013-01-10T18:52:09.370 回答