首页 > 解决方案 > 哪个编译器在模板方法专业化方面是正确的?

问题描述

我没有跟上模板的复杂性,所以只知道足够危险。但是,我遇到了一个问题(由我们的一位测试人员报告),代码在 Xcode(Clang)和 Visual Studio 2019 上正确编译,但行为不同,我想知道哪个是“正确的”

考虑以下简单类:

template <class T>
class foo
{
   void set(int index, T value);
};

该默认模板化设置方法对于大多数类型T的值都适用,但我需要专门针对特定类型的行为,将其称为 String。

所以在相关的 CPP 文件中,我添加了以下内容:

template <>
void foo<String>::set(int index,  String value)
{
   // code here
};

在 Xcode 下,这工作得非常好。使用String的第二个参数调用set正确调用了该专用模板。

但是,使用 Visual Studio 编译失败的完全相同的代码表现不同。使用String参数的同一个调用只是调用了原始而不是专用版本。

我假设在 Xcode 下,编译器正确地创建了一个调用,其签名与 CPP 文件中可用的专用版本相匹配,因此被正确链接。

所以我的问题是:

  1. 为什么这在 Xcode 中有效但在 Visual Studio 中无效
  2. 哪个编译器行为正确,该问题是否应该被视为另一个编译器中的错误?

顺便说一句,我确实尝试在头文件中创建一个显式签名:

template<>
void foo<String>::set(int index, String value );

并且 Xcode 没有抱怨它,一切仍然有效。但是,Visual Studio 虽然没有抱怨该标头签名,但确实在链接时抱怨它找不到匹配的实现,因此:

  1. 为什么那行不通?

虽然我确实找到了一种适用于两种编译器的方法(基本上将内联版本放在头文件中,但我真的很想了解为什么会发生这个问题。

我提前感谢人们。

编辑:吹是我刚刚放在一起的一个真实例子。这在 Xcode 下编译并运行得非常好(特别是对变量 g 正确调用了字符串专业化),但无法在 Visual Studio 2019 中链接并出现以下错误

( public: void __cdecl Foo<class std::basic_string<char,struct std::char_traits,class std::allocator >>::print(int,class std::basic_string<char,struct std::char_traits,class std ::allocator >)" (?print@?$Foo@V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@@QEAAXHV?$basic_string@DU ?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z) 在函数 main 中引用)在函数 main 中引用)

    Classes.h
    ---------
    #pragma once

    #include <iostream>
    #include <string>

    template <class T>
    class Foo
    {
       public:
          void print(int index, T value)
             {
                std::cout << "Using implicit template\n";
             }
    };




    Classes.cpp
    -----------

    #include "Classes.h"


    template<>
    void Foo<std::string>::print(int i, std::string xyz)
    {
       std::cout << "Using explicit String template\n";
    }



    Main.cpp
    --------

    #include <string>
    #include <iostream>

    #include "Sub/Classes.h"

    //==============================================================================
    int main (int argc, char* argv[])
    {

        Foo<double> f;
        
        f.print(1, 2.0);
        
        Foo<std::string> g;
        
        g.print(1, "abc");

        return 0;
    }

标签: c++templatestemplate-specialization

解决方案


您需要在.h文件中转发声明完整的专业化,以告诉其他翻译单元不要实例化通用模板。


template <typename T> struct foo {
  void set(int, T);
};

template <>
void foo<String>::set(int, String);

如果你不这样做,你就违反了单一定义规则。发生这种情况是因为在编译其他.cpp 文件时,编译器不知道存在完整的特化,并实例化通用模板。

说在bar.cpp你写foo<String>().set(1, {});。编译器根据通用模板的定义构造该函数,并将其发送到.oor.obj文件中。在链接时,链接器会看到多个版本的foo<String>::set. 它假设它们是等效的并随机选择一个。你一般不知道它会选择哪个。更糟糕的是,一些调用set可能是内联的,并且调用的版本可能与链接器选择的版本不同。这就是 ODR 违规如此危险的原因。

将前向声明template <> void foo<String>::set(int, String);放在标题中告诉所有翻译单元“不要自动实例化此模板,我保证将此定义放在某个 .cpp 文件中。” 现在bar.cpp不实例化模板。它调用foo<String>::set任何其他前向声明的函数。foo.cpp提供了该完整专业化的定义,并且在任何地方都可以使用。


推荐阅读