一个模板类中单个方法的模板特化

104

假设下面的头文件包含我的模板类,并且至少在两个 .CPP 文件中被引用,那么这段代码可以正确编译:

template <class T>
class TClass 
{
public:
  void doSomething(std::vector<T> * v);
};

template <class T>
void TClass<T>::doSomething(std::vector<T> * v) {
  // Do something with a vector of a generic T
}

template <>
inline void TClass<int>::doSomething(std::vector<int> * v) {
  // Do something with a vector of int's
}

但是请注意专门化方法中的内联。这是必需的,以避免由于该方法被定义多次而导致的链接器错误(在VS2008中为LNK2005)。我理解这一点,因为据我所知,完整的模板专门化与简单的方法定义相同。
那么,我该如何删除该inline?代码不应在每次使用它时重复。我已经搜索了谷歌,在SO中阅读了一些问题,并尝试了许多建议的解决方案,但没有一个成功构建(至少在VS2008中没有)。
谢谢!

4
为什么你想要移除内联样式?是因为你觉得它不美观吗?还是因为你认为它改变了代码的含义? - Martin York
1
因为如果这个方法很“长”,并且在许多地方使用,我将得到它的二进制代码复制到每个地方,对吧?我试图在问题中解释这一点,但我想这不太清楚... :) - Chuim
@Martin: 如果实现需要很多其他代码,而这些代码必须由这个头文件包含而不是cpp文件怎么办? - sbi
6个回答

80

和简单函数一样,您可以使用声明和实现。在头文件声明中放置:

template <>
void TClass<int>::doSomething(std::vector<int> * v);

将实现放入你的 cpp 文件之一:

template <>
void TClass<int>::doSomething(std::vector<int> * v) {
 // Do somtehing with a vector of int's
}

不要忘记删除内联样式 (我曾经忘记过,以为这个解决方案行不通 :))。 在 VC++2005 上进行了检查。


我之前尝试过类似的东西,但是出现了其他错误。不过现在你提到了,我可能忘记在复制粘贴时删除 inline 了。这样就可以工作了! - Chuim
同样适用于模板自由函数(与类方法相对)。我的函数特化也出现了相同的链接器错误。我将函数特化的主体移动到了.cpp文件中,并在头文件中保留了特化的声明,一切都正常了。谢谢! - aldo
我刚遇到了这个问题,上面的解决方法对我很有帮助。此外,您需要注意编译器展开模板代码的位置。如果它被执行两次,编译器会抱怨多个定义。 - Diederik

5

您需要将专业化定义移动到 CPP 文件中。

即使函数未声明为模板,也允许对模板类成员函数进行专业化。


3

没有理由移除关键字inline。
它对代码的含义没有任何影响。


因为如果这个方法会很“长”,并且在许多地方使用,我将会把它的二进制代码复制到每个地方,对吧?我试图在问题中解释这一点,但我想可能不够清楚... :) - Chuim
1
不,链接器会删除任何多余的副本。因此在应用程序或库中,您只会有一个方法实例。 - Martin York
3
如果 inline 关键字导致函数实际内联(标准规定编译器应该将其视为提示),那么这些额外的副本就无法被删除。但是,它只是一个内联的提示(它的主要作用是说“不要以特定方式在链接冲突时生成错误”)。 - Yakk - Adam Nevraumont

2
如果由于任何原因您想删除内联,则 maxim1000 的解决方案完全有效。不过,在您的评论中,似乎您认为内联关键字意味着该函数及其所有内容始终被内联,但据我所知,这实际上非常依赖于编译器优化。
引用自 C++ FAQ:
有几种指定函数为内联的方法,其中一些涉及内联关键字,而其他方法则不涉及。无论如何将函数指定为内联,都是一种请求,允许编译器忽略:编译器可能会在调用指定为内联的函数的某些、全部或者没有地方进行内联扩展。(如果这看起来令人感到无望模糊,请不要气馁。上述灵活性实际上是一个巨大的优势:它使编译器能够将大型函数与小型函数区别对待,此外,如果选择正确的编译器选项,它还可以生成易于调试的代码。)
所以,除非你知道那个函数会让你的可执行文件变得臃肿,或者出于其他原因想要从模板定义头文件中删除它,否则你可以放心地将它保留在原处,不会有任何损害。

2

这与正题有些偏离,但我认为如果能帮助到其他人,还是留在这里吧。我在搜索模板特化时来到了这里,虽然@maxim1000的回答是正确的,最终也帮助我解决了问题,但我认为它并不十分清晰易懂。

我的情况有点不同(但足够类似,我认为可以留下这个答案)。基本上,我正在使用一个第三方库,其中包含各种定义“状态类型”的类。这些类型的核心只是 enum ,但是这些类都继承自一个共同的(抽象)父级,并提供不同的实用函数,例如运算符重载和 static toString(enum type)函数。每个状态 enum 都彼此不同且无关。例如,一个 enum 具有字段 NORMAL,DEGRADED,INOPERABLE ,另一个则具有 AVAILBLE,PENDING,MISSING 等。我的软件负责管理不同组件的不同类型的状态。我想利用这些 enum 类的 toString 函数,但由于它们是抽象的,所以我无法直接实例化它们。我可以扩展我想要使用的每个类,但最终我决定创建一个 template 类,其中 typename 将是我关心的任何具体状态的 enum 。可能会对这个决定产生争议,但我觉得这比扩展每个抽象 enum 类与我自己的自定义类并实现抽象函数要少得多。当然,在我的代码中,我只想能够调用 .toString(enum type)并打印该 enum 的字符串表示形式。由于所有的 enum 都是完全不相关的,它们各自都有自己的 toString 函数(经过一些研究后我了解到)必须使用模板特化来调用。这就导致了我在这里。以下是我必须执行的MCVE,以便使其正常工作。实际上,我的解决方案与@maxim1000的略有不同。

这是一个(大大简化的)枚举头文件。实际上,每个枚举类都在自己的文件中定义。该文件代表了作为我使用的库的一部分提供给我的头文件:
// file enums.h
#include <string>

class Enum1
{
public:
  enum EnumerationItem
  {
    BEARS1,
    BEARS2,
    BEARS3
  };

  static std::string toString(EnumerationItem e)
  {
    // code for converting e to its string representation,
    // omitted for brevity
  }
};

class Enum2
{
public:
  enum EnumerationItem
  {
    TIGERS1,
    TIGERS2,
    TIGERS3
  };

  static std::string toString(EnumerationItem e)
  {
    // code for converting e to its string representation,
    // omitted for brevity
  }
};

添加这行代码只是为了将下一个文件分隔成不同的代码块:

// file TemplateExample.h
#include <string>

template <typename T>
class TemplateExample
{
public:
  TemplateExample(T t);
  virtual ~TemplateExample();

  // this is the function I was most concerned about. Unlike @maxim1000's
  // answer where (s)he declared it outside the class with full template
  // parameters, I was able to keep mine declared in the class just like
  // this
  std::string toString();

private:
  T type_;
};

template <typename T>
TemplateExample<T>::TemplateExample(T t)
  : type_(t)
{

}

template <typename T>
TemplateExample<T>::~TemplateExample()
{

}

下一个文件

// file TemplateExample.cpp
#include <string>

#include "enums.h"
#include "TemplateExample.h"

// for each enum type, I specify a different toString method, and the
// correct one gets called when I call it on that type.
template <>
std::string TemplateExample<Enum1::EnumerationItem>::toString()
{
  return Enum1::toString(type_);
}

template <>
std::string TemplateExample<Enum2::EnumerationItem>::toString()
{
  return Enum2::toString(type_);
}

下一个文件

// and finally, main.cpp
#include <iostream>
#include "TemplateExample.h"
#include "enums.h"

int main()
{
  TemplateExample<Enum1::EnumerationItem> t1(Enum1::EnumerationItem::BEARS1);
  TemplateExample<Enum2::EnumerationItem> t2(Enum2::EnumerationItem::TIGERS3);

  std::cout << t1.toString() << std::endl;
  std::cout << t2.toString() << std::endl;

  return 0;
}

并且这个输出:

BEARS1
TIGERS3

我不确定这是否是解决我的问题的理想解决方案,但对我起作用了。现在,无论我使用多少枚举类型,我只需在 .cpp 文件中添加几行 toString 方法代码,就可以使用已定义的库中的 toString 方法,而无需自己实现它或扩展每个要使用的 enum 类。


1
我想补充一点,如果您打算在头文件中保留特化,仍然有一个很好的理由保留inline关键字。
“直观地说,当您完全专门化某些内容时,它不再依赖于模板参数 - 因此,除非您将专门化内联,否则您需要将其放在.cpp文件中而不是.h文件中,否则您就会违反一个定义规则...”
参考:https://dev59.com/g2855IYBdhLWcg3wQx9L#4445772

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接