【问题标题】:Enforcing function contract at compile time when possible尽可能在编译时强制执行函数契约
【发布时间】:2018-12-26 14:53:24
【问题描述】:

(这个问题的灵感来自How can I generate a compilation error to prevent certain VALUE (not type) to go into the function?

假设我们有一个单参数foo,语义上定义为

int foo(int arg) {
    int* parg;
    if (arg != 5) {
        parg = &arg;
    }

    return *parg;
}

上面的整个代码用于说明一个简单的想法 - 函数返回它自己的参数,除非参数等于 5,在这种情况下行为是未定义的。

现在,挑战 - 以这样的方式修改函数,如果它的参数在编译时已知,则应该生成编译器诊断(警告或错误),如果不是,则行为在运行时保持未定义。解决方案可以依赖于编译器,只要它在四大编译器之一中可用。

以下是一些无法解决问题的潜在路线:

  • 使函数成为模板,将其参数作为模板参数 - 这并不能解决问题,因为它使函数不符合运行时参数的条件
  • 使函数成为constexpr - 这并不能解决问题,因为即使编译器看到未定义的行为,它们也不会在我的测试中产生诊断 - 相反,gcc 会插入 ud2 指令,这不是我想要的.

【问题讨论】:

  • 出于好奇,为什么这个 Q 被赞成,而链接一个被反对?
  • @appleapple 因为这个 #1 是精心制定的,所以 #2 给了一个简短的足够的上下文,而 #3 定义了一个 precise (SMART) objective
  • @YSC 好吧,这个问题没有显示任何研究工作;不清楚或没用。如果我必须投一个,我会投反对票。
  • @appleapple 你不必投一个,但如果你愿意,可以随意投。我想这是一个品味问题。我发现它很有用(我从未成功完成该任务,我认为如果有一个 API 可以通过编译器错误来生成与价值相关的合同会很好)。
  • @IłyaBursov - 不是重复的,建议的答案不能解决问题。

标签: c++


【解决方案1】:

constexpr 用于 常量表达式 时出现错误:

constexpr int foo(int arg) {
    int* parg = nullptr;
    if (arg != 5) {
        parg = &arg;
    }
    return *parg;
}

Demo

我们无法知道参数值在编译类型时是已知的,但我们可以使用类型表示值,std::integral_constant

// alias to shorten name. 
template <int N>
using int_c = std::integral_constant<int, N>;

可能有UDLoperator "" _c5_c,42_c

然后,添加重载:

template <int N>
constexpr auto foo(int_c<N>) {
    return int_c<foo(N)>{};
}

所以:

foo(int_c<42>{}); // OK
foo(int_c<5>{}); // Fail to compile

// and with previous constexpr:
foo(5); // Runtime error, No compile time diagnostic
constexpr auto r = foo(5); // Fail to compile

正如我所说,参数在函数内部不知道是常量,is_constexpr seems not possible in standard 允许调度,但是一些编译器为此提供了内置的 (__builtin_constant_p),所以使用 MACRO,我们可以进行调度:

#define FOO(X) [&](){ \
    if constexpr (__builtin_constant_p(X)) {\
        return foo(int_c<__builtin_constant_p (X) ? X : 0>{});\
    } else {\
        return foo(X); \
    } \
}()

Demo

注意:不能直接使用foo(int_c&lt;X&gt;{}),即使在 if constexpr 中,因为还有一些语法检查。

【讨论】:

  • Consexpr 版本不会给我任何诊断 gcc.godbolt.org/z/ZCho3b 当它的结果不用于初始化常量表达式变量时。
  • 我明白这一点,这正是我所指的 - 除非函数以常量表达式调用,否则不提供诊断
  • 这就是为什么我在回答中提供传递编译时参数(使用int_c)的原因,它允许在编译时检查,因为他们用常量表达式调用它。
  • Jarod,可能是我说得不够清楚。目标是有一个可调用的函数,它会在可能的情况下在编译时强制执行合约(即参数是已知的)。您的解决方案有效地引入了两个重载,并且成功的编译时检查取决于开发人员调用第二个重载的纪律。
  • 找到了一种内置 gcc 的方法,由 clang 支持(因此适用于 clang,但不适用于 g++ ;-))
【解决方案2】:

gcc/clang/intel 编译器支持__builtin_constant_p,所以你可以使用类似的东西:

template <int D>
int foo_ub(int arg) {
    static_assert(D != 5, "error");
    int* parg = nullptr;
    if (arg != 5) {
        parg = &arg;
    }

    return *parg;
}

#define foo(e) foo_ub< __builtin_constant_p(e) ? e : 0 >(e)

这些语句产生编译时错误:

  • foo(5)
  • foo(2+3)
  • constexpr int i = 5; foo(i);

而所有其他 - 运行时段错误(如果没有使用 nullptr,则为 ub)

【讨论】:

    【解决方案3】:

    它并不完美它要求我们在两个不同的地方使用参数,但它'有效'

    template<int N = 0>
    int foo(int arg = 0) {
        static_assert(N != 5, "N cannot be 5!");
        int* parg;
        if (arg != 5) {
            parg = &arg;
        }
    
        return *parg;
    }
    

    我们可以这样称呼它:

    foo<5>();   // does not compile
    foo(5);     // UB
    foo<5>(5);  // does not compile
    foo<5>(10); // does not compile
    foo<10>(5); // UB
    foo();      // fine
    foo<10>();  // fine
    foo(10);    // fine
    

    【讨论】:

    • 不,它不起作用。行为取决于程序员的纪律(确保在编译时知道模板参数时提供模板参数)。而不是这种方法,如果程序员有纪律,我只需要两个函数 - 模板化和非模板化。
    • 公平,谢谢您的评论。我完全同意这实际上并不能解决整个问题,但我会将其作为中性提示/信息留给未来的访问者:>
    • 您可以添加额外的运行时检查参数是否相等或默认。
    猜你喜欢
    • 2014-11-05
    • 2012-12-27
    • 1970-01-01
    • 2017-01-14
    • 1970-01-01
    • 1970-01-01
    • 2016-07-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多