解析模板化类的特化静态成员变量的定义

Resolving Definitions of Specialized Static Member Variables of Templated Classes

本文关键字:变量 定义 静态成员      更新时间:2023-10-16

编译器之战十四:双重定义的毁灭,联合主演《可疑宣言》!

编译器,所有的-O0或Debug模式:

  • g++ 5.2.0
  • clang++ 3.6.0
  • vc++ 18.00.40629 (MSVC 2013, Update 5)

简介:

  • vc++在拒绝声明和定义模板化类的专用静态成员变量的语法是错误的吗?
template <> const std::string TemplatedClass::x; // .h file
template <> const std::string TemplatedClass::x= "string"; // .cpp file
  • 删除头文件中的声明是否会导致定义良好的程序格式错误?
  • 如果是这样,是否有一个vc++友好的方式来声明一个模板化类的静态成员变量的专门化?

当我对定义模板的专门化静态成员变量的问题进行MCVE时,我遇到了vc++, GCC和Clang之间关于声明所说的专门化静态成员变量的有趣的行为变化。具体来说,语法

template <> const std::string TemplatedClass<int>::x; // .h file
template <> const std::string TemplatedClass<int>::x= "string"; // .cpp file        

似乎致命地冒犯了vc++, vc++对多重定义的抱怨回应:

error C2374: 'member' : redefinition; multiple initialization

,而GCC和clang都可以从容应对。

研究

我假设后两个是正确的,因为它们通常是正确的,而且因为上面的语法来自一个关于专用模板类的静态成员初始化的答案,它引用了2010年标准的14.7.3/15段,指出template<> X Q<int>::x是声明,而不是定义。我冒昧地找到了N4296号草案中相同的段落,认为它可能在这段时间里发生了变化。确实如此,但只是将两段文字往前移了两段,并包含了额外的说明:

14.7.3/13

模板的静态数据成员的显式特化,或者静态数据成员模板的显式特化,如果声明包含初始化式,则是定义;否则,它是一个声明。[注意:需要默认初始化的模板的静态数据成员的定义必须使用大括号-init-list:

template<> X Q<int>::x;      // declaration
template<> X Q<int>::x ();   // error: declares a function
template<> X Q<int>::x { };  // definition

这对我来说似乎很清楚,但vc++似乎有不同的解释。我试着简单地注释掉这个令人不快的声明,没有编译器抱怨,这似乎解决了我的问题,但并没有,因为第6段有这样的说法:(令人担忧的强调我的)

14.7.3/6

如果模板、成员模板或类模板的成员被显式特化,则在第一次使用该特化之前声明该特化,该特化会导致隐式实例化发生,在每个使用该特化的翻译单元中;不需要诊断。如果程序没有提供显式专门化的定义,并且专门化的使用方式会导致隐式实例化发生,或者成员是虚成员函数,则程序是病态的,不需要诊断。对于声明但未定义的显式专门化,永远不会生成隐式实例化。

它提供了一些例子,但它们都是针对使用后的函数或模板化类型的成员枚举和类的专门化的,我很确定这不适用于这个问题。然而,p13的开头几个字似乎暗示,专门化静态成员变量的声明也是一种显式专门化,至少在使用所说明的语法时是这样。

MCVE

我用于实验的测试可以在Coliru上找到,因为相当复杂的命令行向StackedCrooked道歉。

main.cpp

#include <iostream>
// 'header' file
#include "test.h"
int main(){
  std::cout << test::FruitNames<Fruit::APPLE>::lowercaseName();
}

test.h(未注释)
Test.h(声明注释掉)

#ifndef TEMPLATE_TEST
#define TEMPLATE_TEST
#include <algorithm>
#include <locale>
#include <string>
namespace test{
  enum class Fruits{
    APPLE
  };
  template <Fruits FruitType_>
  class FruitNames{
    static const std::string name_;
  /*...*/
  public:
    static std::string lowercaseName() {/*...uses name_...*/}
  };
    // should be counted as declaration. VC++ doesn't.
  template <> const std::string FruitNames<Fruits::APPLE>::name_;
} // end namespace test
#endif // TEMPLATE_TEST

test.cpp

#include "test.h"
namespace test{
  template <> const std::string FruitNames<Fruits::APPLE>::name_ = "Apple";
}

输出

gcc和clang都会输出

apple

在test.h中有或没有专门化声明。如果test.h中的声明被注释掉了,vc++会这样做,但如果它存在,则会产生双初始化错误。

最后

  • vc++拒绝模板化类的静态成员变量的声明/显式专门化语法是不正确的吗?还是允许但不是强制的诊断错误?
  • 删除声明是否会导致程序失效不规范的?
  • 如果没有声明它是不正确的,我如何让vc++与明确的计划?
如前所述,vc++拒绝模板化类的静态成员变量的声明/显式专门化语法是不正确的,还是允许但不是强制的诊断错误?

是的,这是vc++中的一个bug。在Visual Studio 2019 version 16.5 Preview 2中已经修复了这个问题。


删除声明会导致程序是病态的吗?

你方的标准报价似乎表明……其他人同意。


如果没有声明它是病态的,我如何让vc++在一个定义良好的程序中发挥良好?

作为一种变通方法,您可以对整个类进行专门化,然后在不使用template<>语法的情况下定义成员。参见Amir Kirsh对类似问题的回答:https://stackoverflow.com/a/58583521/758345

或者,您可以在头文件中定义和初始化变量,并将其标记为内联(自c++17起):

template <> inline const std::string TemplatedClass::x = "string"; // .h file