Non-Profit, International

Spirit unsterblich.

Contracts

字数统计:1486 blog

最近 C++ 标准委员会的线下会议上通过了契约(本文译法,有的地方也翻译成合同),可以说是 C++26 最重要的一个特性,因此还是非常有必要单独拿出来讲一讲。其本身设计涵盖语言的各个方面,就结果而言,契约本身设计还是非常简单而且轻量的,使用起来并不会太困难。

在讲 C++26 的特性之前,还是需要讲一下背景知识。在 C++ 中,函数根据允许进行何种输入分为两类:宽契约(wide contract)和窄契约(narrow contract),宽契约指的是函数对任何输入都有明确定义的行为(异常也算明确定义),窄契约是函数对于一些输入有不确定的行为。当然在现实中宽和窄都是相对的,因为未定义行为具有传染性,例如通过错误的指针调用任何具有宽契约的函数显然也是未定义的。

核心语言

契约给标准加入了一个关键词 contract_assert,基本可以认为是 assert 宏的替代品,以及两个在函数声明上使用的上下文关键词 prepost,前者用于表达函数的前条件,后者用于表达后条件。

contract_assert 的语法是:

contract_assert 属性列表(可选)( 条件 ) ;

如果 contract_assert 断言失败,条件求值的结果是 false,就会调用契约违反处理函数,后面讲。

postpre 的语法是:

pre( 条件 )

post( 条件 )

post( 返回值标识符 : 条件 )

prepost 写在函数的参数列表的括号后面,比如:


int foo(int x) pre(x >= 0) post(r : r < 0)
{
    x--;
    return x;
}

pre 在执行函数体前调用,post 在函数返回后调用。如果条件求值的结果是 false,那么就会调用契约违反处理函数。prepost 可以出现多次,按编写顺序进行检查。

post 可以使用冒号引用返回值以检查返回值作为后条件,也就是上面例子中的 (r : r < 0)

构造函数声明中的 prepost 没有隐式 this,lambda 的 prepost 不使用默认捕获,必须显式捕获才能用。

虚函数目前不能写 prepost,显式默认化(= default)函数也不能写。

对于成员初始化列表,要写在契约后面:


struct x
{
    int y = 1;
    x() pre( this->y == 1) : y( 2 ) {}
};

函数多次声明时契约必须匹配。

C++ 标准的意思就是让你在 prepost 的条件中去调用具有窄契约的函数,来扩宽函数本身的契约。

契约有四种模式:ignore,observe,enforce 和 quick-enforce。默认是 enforce,即打印出信息,调用契约违反函数并终止程序;ignore 是不检查契约,直接忽略;observe 是违反契约后继续运行程序;quick-enforce 是违反契约时直接终止程序。等到编译器实现的时候会提供编译选项控制使用什么模式。当然,这四个模式具体什么行为也是编译器说的算。

契约比较特殊的是它会捕获契约条件中抛出的异常,阻止异常向外抛出。

标准库

标准库新定义了一个全局函数:

void handle_contract_violation (std::contracts::contract_violation const&) noexcept;

该函数没有声明,实现可以决定它是否是可替换的。

可替换指的是它类似 void* ::operator new(std::size_t),标准库提供了一个默认实现,但用户可以定义一个自己的。因此当 handle_contract_violation 是可替换时,每个程序也只能定义一次,否则会链接冲突。

当程序检查出违反契约时(除了在常量求值时会直接发出诊断消息),就会调用 handle_contract_violation

默认的 handle_contract_violation 的行为是实现定义的,一般来说它会打印消息然后返回(终止程序由编译器负责,不需要在这个函数内手动终止)。

标准库新加了 <contracts> 头文件,概要如下:


namespace std::contracts
{
// 契约来源 
enum class assertion_kind : /* 未指定 */
{
    pre = 1,   // 指示是 pre 前条件违反
    post = 2,  // 后条件违反
    assert = 3 // 断言违反
};
// 契约使用的模式
enum class evaluation_semantic : /* 未指定 */
{
    ignore = 1,
    observe = 2,
    enforce = 3,
    quick_enforce = 4
};
// 契约违反的原因
enum class detection_mode : /* 未指定 */
{
    predicate_false = 1,     // 断言失败
    evaluation_exception = 2 // 捕获异常
};
class contract_violation
{
    // 用户不能主动构造
  public:
    contract_violation(const contract_violation &) = delete;
    contract_violation &operator=(const contract_violation &) = delete;
    ~contract_violation();
    const char *comment() const noexcept;
    contracts::detection_mode detection_mode() const noexcept;
    exception_ptr evaluation_exception() const noexcept;
    bool is_terminating() const noexcept;
    assertion_kind kind() const noexcept;
    source_location location() const noexcept;
    evaluation_semantic semantic() const noexcept;
};
void invoke_default_contract_violation_handler(const contract_violation &);
}

comment 函数返回实现定义的字符串,可以是字符串形式的契约条件,evaluation_exception 可以获得被契约捕获的异常,is_terminating 等于判断契约模式是不是 enforce。实际上当模式为 ignore 和 quick-enforce 时都不会调用契约违反处理函数。剩下的函数的功能自己意会。

功能特性测试宏

如果编译器实现了契约,那么编译器会预定义功能特性测试宏 __cpp_contracts202502L

参考:

若无特殊声明,本人原创文章以 CC BY-SA 3.0 许可协议 提供。