9

在模板编程中,static_assert帮助程序员检查模板参数的约束,并在违反约束时生成人类可读的错误消息。

考虑这段代码,

template<typename T>
void f(T)
{
    static_assert(T(), "first requirement failed to meet.");

    static_assert(T::value, "second requirement failed to meet.");    

    T t = 10; //even this may generate error!
}

我的想法是:如果第一次static_assert失败,这意味着不满足某些要求,因此编译应该停止,只生成第一条错误消息——因为继续编译只是为了生成越来越多的信息没有多大意义错误消息,其中大多数通常指向单个约束违规。数百条错误消息,而不仅仅是一条,在屏幕上看起来非常可怕——我什至会说,它在某种程度上违背了它的目的Tstatic_assert

例如,如果我将上述函数模板称为:

f(std::false_type{});

GCC 4.8 生成以下内容:

main.cpp: In instantiation of 'void f(T) [with T = std::integral_constant<bool, false>]':
main.cpp:16:24:   required from here
main.cpp:7:5: error: static assertion failed: first requirement failed to meet.
     static_assert(T(), "first requirement failed to meet.");
     ^
main.cpp:9:5: error: static assertion failed: second requirement failed to meet.
     static_assert(T::value, "second requirement failed to meet.");    
     ^
main.cpp:11:11: error: conversion from 'int' to non-scalar type 'std::integral_constant<bool, false>' requested
     T t = 10; //even this may generate error!

正如你所看到的(在线),这是太多的错误。如果第一个static_assert失败,如果继续编译,很可能其余代码也会失败,那为什么还要继续编译呢?在模板编程中,我相信很多程序员都不想要这样的级联错误信息!

我试图通过将函数拆分为多个函数来解决这个问题,每个函数只检查一个约束,如:

template<typename T>
void f_impl(T); //forward declaration

template<typename T>
void f(T)
{
    static_assert(T(), "first requirement failed to meet.");
    f_impl(T());
}

template<typename T>
void f_impl(T)
{
    static_assert(T::value, "second requirement failed to meet.");     
    T t = 10;
}  

f(std::false_type{}); //call

现在这会生成:

main.cpp: In instantiation of 'void f(T) [with T = std::integral_constant<bool, false>]':
main.cpp:24:24:   required from here
main.cpp:10:5: error: static assertion failed: first requirement failed to meet.
     static_assert(T(), "first requirement failed to meet.");
     ^

这是一个很大的改进——只有一条错误消息更容易阅读和理解(参见在线)。

我的问题是,

  • 为什么编译不会停在第一个static_assert
  • 由于拆分函数模板并检查每个 function_impl 中的一个约束,有助于GCC 和 clang仍然会 产生大量错误,有没有办法以更一致的方式改进诊断——适用于所有编译器的东西?
4

2 回答 2

4

这里需要平衡多个目标。特别是,可以通过在第一个错误上停止来获得更小更简单的错误消息,这很好。同时,在第一个错误上停止不会为您提供有关在尝试另一个可能昂贵的编译之前您可能想要解决的任何其他问题的信息。例如,在您的第一个示例中,我个人更喜欢static_assert一次检查所有 s。将错误消息读取为:

您未能满足以下要求

  • 默认构造函数
  • 嵌套value类型

我宁愿在第一遍中检测到这两个错误,而不是修复一个并且必须花费几分钟让构建系统在下一个过程中跳闸。

这里的前提是编译器能够从错误中恢复并继续解析,尽管语法依赖于上下文并且并非总是如此,所以问题的一部分消极方面是您可以信任第一个错误,但是下一个错误可能只是第一个错误的结果,需要经验才能意识到哪个是哪个。

所有这些都是实现的质量(因此取决于编译器),并且许多实现让您确定何时停止,因此由用户和传递给编译器的标志决定。编译器正在更好地报告错误并从中恢复,因此您可以期待这里的改进。为了进一步改进,> C++14(C++17?以后?)将添加旨在改进错误消息的概念。

总结:

  • 这是实现的质量,可以通过编译器标志来控制
  • 不是每个人都想要你想要的,有些人想要在每个编译器传递中检测多个错误
  • 未来会出现更好的错误信息(概念、编译器改进)
于 2013-12-02T19:22:51.143 回答
4

我同意 David Rodríguez - dribeas 并为编译器作者辩护考虑这个例子:

#include <type_traits>

class A {};

// I want the nice error message below in several functions.
// Instead of repeating myself, let's put it in a function.
template <typename U>
void check() {
    static_assert(std::is_convertible<U*, const volatile A*>::value,
        "U doesn't derive publicly from A "
        "(did you forget to include it's header file?)");
}

template <typename U>
void f(U* u) {
    // check legality (with a nice error message)
    check<U>();
    // before trying a failing initialization:
    A* p = u;
}

class B; // I forget to include "B.h"

int main() {
    B* b = nullptr;
    f(b);
}

当实例化f<B>启动编译器(或编译器编写者)可能会想:“嗯……我需要实例化check<U>,人们总是抱怨编译模板太慢。所以我会继续下去,也许下面有问题,我事件不需要实例化check。”

我相信上面的推理是有道理的。(请注意,我不是编译器编写者,所以我只是在这里推测)。

GCC 4.8 和 VS2010 都在继续编译f<B>,推迟实例化以check<B>备后用。然后他们找到失败的初始化并提供他们自己的错误消息。VS2010 立即停止,我没有收到很好的错误消息!GCC 继续运行并产生我想要的消息(但仅在它自己之后)。

元编程对于程序员和编译器来说都是棘手的。有很大static_assert帮助,但它不是灵丹妙药。

于 2013-12-02T20:00:44.177 回答