尽管编译时间常量 static_assert,专用模板*不*断言

Specialised template *not* asserting despite compile time constant static_assert

几天来,我一直在使用和测试我的应用程序,使用以下代码没有遇到任何问题:

class dataHandler
{
public:
    template<class T>
    T GetData(bool truncate = false) { static_assert(false, "Unhandled output type"); }
    template<T>
    int GetData<int>(bool truncate)
    {
        // Normal stuff that compiles
    }
}

只要不执行 GetData 默认值的隐式实例化,这(正如我当时预期的那样)就可以正常工作。然而,今天在专门为 void* 添加了一个新的失败专业化之后(我想要一个不同的错误)我发现它不会由于断言而编译,即使代码中从未调用或提及过 `void* 规范。

我开始了一个新的 C++ 测试项目(具有相同的 C++ 版本、MSVC 版本和 VS2022 版本),发现它也因为同样的原因无法编译:

template<class T>
T Spec()
{
    static_assert(false, "default");
}
template<>
int Spec<int>()
{
    return 1;
}

我无法在测试项目中尝试的任何内容中复制原始 'success',并且初始项目中失败的 GetData<void*> 断言似乎表明它不是项目配置/差异在工具集问题中。

经过一番搜索后,我发现失败的原因是预期的(或其他预期的)行为,以及解决方法。

但是我发现自己想知道 为什么 static_assert(false) 的初始情况也没有编译失败。这是一些小众例外,还是 MSVC 的一致性问题?

However I find myself wondering why the initial case of static_assert(false) didn't also fail to compile. Is this some niche exception, or is this an issue with MSVC's consistency?

这当然不是矛盾。这里的关键部分是“标准一致性模式”,可通过编译器选项/permissive- 进行控制。 See the documentation of this compiler option.

VS2017 之前的 MSVC 接受您的 static_assert(false, ...) 的原因是因为它将函数模板的内容解析推迟到模板实例化。这就是解析器过去的工作方式,也是为什么 two-phase 名称查找等功能永远无法正确实现的原因。我会推荐你​​去 this blog post 了解更多这方面的背景知识。

您可以轻松地自己尝试一下。即使函数包含 ill-formed 甚至不应该正确解析的代码,只要模板未被实例化,编译器就不会抱怨:

template<class T>
void foo()
{
    Normally this should not (never actually (compile)) but it does anyway
}

它似乎做了一些基本的括号匹配,仅此而已。在实例化foo时(通常是在调用时)记录和解析令牌流。

为了修复 two-phase 查找(和其他一致性问题),他们必须修复编译器并在 遇到 函数模板时解析代码,而不是而不是 实例化 。但现在他们遇到了问题,因为很多旧代码可能依赖于突然不再编译的旧编译器行为。因此,他们为用户引入了 /permissive- 选项以 opt-in 进入标准一致性模式。并且逐渐更改了新项目或某些编译器选项的默认值,如可以在文档中阅读:

The /permissive- option is implicitly set by the /std:c++latest option starting in Visual Studio 2019 version 16.8, and in version 16.11 by the /std:c++20 option. /permissive- is required for C++20 Modules support. Perhaps your code doesn't need modules support but requires other features enabled under /std:c++20 or /std:c++latest. You can explicitly enable Microsoft extension support by using the /permissive option without the trailing dash. The /permissive option must come after any option that sets /permissive- implicitly.

By default, the /permissive- option is set in new projects created by Visual Studio 2017 version 15.5 and later versions. It's not set by default in earlier versions. When the option is set, the compiler generates diagnostic errors or warnings when non-standard language constructs are detected in your code. These constructs include some common bugs in pre-C++11 code.

这让我们找到了您问题的答案。您没有在原始代码中看到它,因为您在没有 /permissive- 的情况下进行编译。在您的测试项目中,在 VS2022 中创建,默认设置 /permissive- 模式,因此无法编译。它也无法在显式特化中编译(您的 void* 情况),因为此时模板参数已知并且函数被实例化。

有几种方法可以正确修复您的代码。一种是明确删除主模板和任何您不想拥有的专业。

template<class T> void foo() = delete;
template<> void foo<void*>() = delete;
template<> void foo<int>()
{
    // ...
}

这将使用任何显式删除的变体 ill-formed,而不会包含错误消息。当然,在这个例子中 void* 的情况有点多余。

如果你想坚持static_assert,你必须使断言的条件依赖于模板参数。如果你用 C++17 编译,你可以这样做:

template<class...> constexpr bool always_false = false;
template<class T> void foo()
{
    static_assert(always_false<T>, "your error here");
}

如果使用 C++14 或更早版本,您可以将 always_false 包装到结构模板中:

#include <type_traits>
template<class...> struct always_false : false_type { };

template<class T> void foo()
{
    static_assert(always_false<T>::value, "your error here");
}