在 Visual Studio 2019 中使用枚举作为模板参数时出现错误 C2440 和 C2973

Posted

技术标签:

【中文标题】在 Visual Studio 2019 中使用枚举作为模板参数时出现错误 C2440 和 C2973【英文标题】:Error C2440 and C2973 when using enum as template argument in visual studio 2019 【发布时间】:2021-03-21 09:41:48 【问题描述】:

使用 Visual Studio 2019 编译此代码时出现两个错误 C2440 和 C2973:

#include <iostream>
#include <array>

enum class First 
    SOME_VALUE,
    OTHER_VALUE
;

enum class Second 
    PRIME,
    BIS
;


template<First FIRST_VAL>
struct FirstData 
    int i;
;

template<Second SECOND_VAL>
struct SecondData 
    long l;
;

template<First FIRST_VAL,Second SECOND_VAL>
class Data :public SecondData<SECOND_VAL>, public FirstData<FIRST_VAL> 

;

template<First FIRST_VAL>
struct WithFirstData 
    virtual const FirstData<FIRST_VAL >& getData() = 0;
    virtual ~WithFirstData() = default;
;

template<Second SECOND_VAL>
struct WithSecondData 
    virtual const SecondData<SECOND_VAL>& getData() = 0;
    virtual ~WithSecondData() = default;
;

template<First FIRST_VAL, Second SECOND_VAL>
class WithData : public WithFirstData<FIRST_VAL>, public WithSecondData<SECOND_VAL> 

public:
    const First First;
    const Second Second;
private:

    const Data<FIRST_VAL, SECOND_VAL> data;
public:

    virtual const Data<FIRST_VAL, SECOND_VAL>& getData() 
        return data;
    

    template<typename ... DataArgs>
    WithData(DataArgs &&... data) :First(FIRST_VAL), Second(SECOND_VAL), data(std::forward<DataArgs>(data)...)     

    virtual ~WithData() ;
;

class ConcreteWithData : public WithData<First::SOME_VALUE, Second::BIS> 
public:
    ConcreteWithData() : WithData() 
    
;

std::ostream& operator<<(std::ostream& ostream, const ConcreteWithData& o) 
    ostream << static_cast<int>(o.First);
    return ostream;

int main() 

    ConcreteWithData ev;
    std::cout << "Hello World!\n" << ev;

错误消息除外:

1>C:\[...]\Test.cpp(55): error C2440: 'specialization': cannot convert from 'int' to 'First'
1>C:\[...]\Test.cpp(55): message : Conversion to enumeration type requires an explicit cast (static_cast, C-style cast or function-style cast)
1>C:\[...]\Test.cpp(55): error C2440: 'specialization': cannot convert from 'int' to 'Second'
1>C:\[...]\Test.cpp(55): message : Conversion to enumeration type requires an explicit cast (static_cast, C-style cast or function-style cast)
1>C:\[...]\Test.cpp(55): error C2973: 'Event': invalid template argument 'int'
1>C:\[...]\Test.cpp(44): message : see declaration of 'Event'
1>C:\[...]\Test.cpp(44): error C2440: 'specialization': cannot convert from 'int' to 'First'
1>C:\[...]\Test.cpp(44,36): message : Conversion to enumeration type requires an explicit cast (static_cast, C-style cast or function-style cast)
1>C:\[...]\Test.cpp(44): error C2440: 'specialization': cannot convert from 'int' to 'Second'
1>C:\[...]\Test.cpp(44,70): message : Conversion to enumeration type requires an explicit cast (static_cast, C-style cast or function-style cast)
1>C:\[...]\Test.cpp(44,1): error C2973: 'WithSecondData': invalid template argument 'int'
1>C:\[...]\Test.cpp(38): message : see declaration of 'WithSecondData'

有趣的是,这在 gcc (g++.exe (Rev6, Built by MSYS2 project) 10.2.0) 下编译没有问题。 谁有错?我做了编译器不应该理解的事情;或 vs 编译器无法正确理解枚举;还是 gcc 允许它不应该做的事情?

编辑 2:

如果我将它重构为无范围的枚举和整数作为模板参数,这将编译,因此有一种解决方法,但我希望能够使用范围枚举。而且,更重要的是,我很想首先了解哪里出了问题。

编辑 1:

原始代码有点大,我把它简化了一点,但基本上我想要实现的是根据不同的枚举值对 FirstData 和 SecondData 的不同数据进行专门化。因此,在引用不同的具体实现时,我可以使用对 WithFirstData&lt;SOME_VALUE&gt; WithSecondData&lt;PRIME&gt; 的引用,并确保这些实现具有我正在寻找的数据。

【问题讨论】:

我不太清楚你想用该代码实现什么。介意在你的问题中详细说明一下吗? 您的代码还有许多其他问题,但看起来确实像编译器错误。 clang 和 gcc 在修复错误后进行编译。 我建议您可以将此问题提交给Microsoft DC。 【参考方案1】:

tl;dr:在https://godbolt.org/z/ez1K43z8T查看解决方案。

我知道此时这是一个相当老的问题,但由于我最近在我的一个项目中遇到了同样的问题,我想讨论我的发现和可能遇到类似问题的其他人的潜在解决方法。

首先要注意的是,正如另一位用户在 cmets 中提到的那样,这似乎是 MSVC 的一个错误。我无法详细说明为什么会发生这种情况,因为我自己并不在编译器上工作,但根据我自己的发现,它似乎在内部将作用域枚举转换为它们的底层类型。

让我们仔细看看编译器的错误信息之一:

C:\[...]\Test.cpp(55): error C2440: 'specialization': cannot convert from 'int' to 'First'
C:\[...]\Test.cpp(55): message : Conversion to enumeration type requires an explicit cast (static_cast, C-style cast or function-style cast)

由于某些莫名其妙的原因,MSVC 试图将 int 值从作用域枚举 First 转换为枚举值。但是,为什么它是从 int 变量中执行的呢?这是因为int 是作用域枚举的默认基础类型:

static_assert(std::is_same_v<std::to_underlying_t<First>, int>);  // The assertion succeeds!

因此,作为一种解决方法,这里有一个 C++20 概念,它比较两种类型,其中至少一种是枚举,并检查 MSVC 是否会为了工作而将它们视为“等效”围绕这个问题:

template <typename LHSType, typename RHSType>
concept IsEnumEquivalent = (std::is_enum_v<LHSType> && !std::is_enum_v<RHSType> && std::is_same_v<std::underlying_type_t<LHSType>, RHSType>) ||
    (std::is_enum_v<RHSType> && !std::is_enum_v<LHSType> && std::is_same_v<std::underlying_type_t<RHSType>, LHSType>) ||
    (std::is_enum_v<LHSType> && std::is_enum_v<RHSType> && std::is_same_v<LHSType, RHSType>);

然后,您可以像这样定义 FirstDataSecondData 结构:

template <IsEnumEquivalent<First> FIRST_TYPE, FIRST_TYPE FIRST_VAL>
struct FirstData 

    static constexpr int i = 0;
;

template <IsEnumEquivalent<Second> SECOND_TYPE, SECOND_TYPE SECOND_VAL>
struct SecondData 

    static constexpr long l = 0;
;

连同任何显式的模板特化:

template <>
struct FirstData<First, First::SOME_VALUE>

    static constexpr int i = 5;
;

// Continue defining explicit specializations...

这就是解决 MSVC 错误的内容。但是,由于您提到了一个更具体的问题,我将讨论如何使用此解决方案来解决它。正如 OP 所提到的,目标是让具体实现能够访问特定数据,大概是在编译时定义的。 (如果数据应该在运行时可以修改,那么模板元编程可能不是您首先要寻找的答案。)

需要注意的一个有趣的事情是,我们的概念IsEnumEquivalent 将接受int 值,如果用户希望提供它们。考虑到这样做的全部目的是利用作用域枚举的类型安全性,最好防止这种情况发生。一种方法如下:

namespace IMPL

    template <IsEnumEquivalent<First> FIRST_TYPE, FIRST_TYPE FIRST_VAL, IsEnumEquivalent<Second> SECOND_TYPE, SECOND_TYPE SECOND_VAL>
    struct WithDataIMPL
    
        static constexpr FirstData<FIRST_TYPE, FIRST_VAL> FirstDataElement;
        static constexpr SecondData<SECOND_TYPE, SECOND_VAL> SecondDataElement;
    ;


template <First FIRST_VAL, Second SECOND_VAL>
using WithData = IMPL::WithDataIMPL<First, FIRST_VAL, Second, SECOND_VAL>;

本质上,我们将WithData 定义为IMPL 命名空间中包含的实现结构的类型别名。这是我个人组织实施细节的惯例;我发现的大多数其他项目都将其称为 details 命名空间或类似名称。无论您采用何种约定,其理念都是对 API 用户隐藏实现细节。

如果您使用的是 C++20 模块,那么您实际上可以通过选择不导出它们来防止其他模块看到这些实现细节!但是,如果您仍在使用传统的头文件,那么您将不得不忽略不可避免地泄漏到其他源文件中的任何细节。

不管怎样,回到原来的问题,我们可以定义一个“具体实现”类如下:

class ConcreteWithData : public WithData<First::SOME_VALUE, Second::BIS> 

    // Do whatever you want with ConcreteWithData here. However, if you don't
    // plan on adding anything else to this class, then it may be better to just
    // make it a type alias:
    //
    // using ConcreteWithData = WithData<First::SOME_VALUE, Second::BIS>;
;

然后按如下方式访问其数据:

std::ostream& operator<<(std::ostream& ostream, const ConcreteWithData& o) 

    ostream << o.FirstDataElement.i << '\n';
    ostream << o.SecondDataElement.l << '\n';

    return ostream;

哇,这篇文章最终比我预期的要长很多。希望它最终对遇到/正在遇到与 OP 相同问题的其他人有所帮助。如果您想了解更多详细信息,以下 Godbolt 链接包含此解决方案的更详细说明,因为它与此特定问题有关:https://godbolt.org/z/ez1K43z8T。

【讨论】:

以上是关于在 Visual Studio 2019 中使用枚举作为模板参数时出现错误 C2440 和 C2973的主要内容,如果未能解决你的问题,请参考以下文章

Visual Studio 2017工程在Visual Studio 2019打开碰到的问题

在 Visual Studio 2019 中使用 Qt 时出错

如何在 Visual Studio 2019 中使用 C#10

如何在 Visual Studio 2019 中使用 CppFlow 库?

在 Visual Studio 2019 中使用新项目创建 Git 存储库

visualstudio的窗体设计中led在哪里