Boost C++ 库

……是世界上备受推崇且设计精湛的 C++ 库项目之一。 Herb SutterAndrei Alexandrescu, C++ Coding Standards

第一章. Boost.Contract 1.0.0 - Boost C++ 函数库
Next

第一章. Boost.Contract 1.0.0

Lorenzo Caminiti

根据 Boost 软件许可证版本 1.0 分发(参见附带文件 LICENSE_1_0.txt 或副本位于 https://boost.ac.cn/LICENSE_1_0.txt

我们的领域需要更多的正式性,但这个行业尚未认识到这一点。

-- Bertrand Meyer (参见 [Meyer97] 第 400 页)

此库为 C++ 编程语言实现了契约式编程(又称“Design by Contract”或 DbC)。此库支持所有契约式编程功能:子契约、类不变量(也适用于静态和 volatile 成员函数)、后置条件(包含旧值和返回值)、前置条件、断言失败时的可定制操作(例如,终止程序或抛出异常)、断言的可选编译、禁用断言(在检查其他断言时避免无限递归)等(参见 功能摘要)。

契约式编程允许指定前置条件、后置条件和类不变量,这些将在函数运行时自动进行检查。这些条件在源代码本身内断言程序规范,从而在测试期间更快地发现错误,使代码具有自文档化特性,并提高整体软件质量(参见 契约式编程概述)。

例如,考虑以下函数 inc,它将参数 x 增加 1,让我们使用代码注释来编写其契约(参见 introduction_comments.cpp)。

void inc(int& x)
    // Precondition:    x < std::numeric_limit<int>::max()
    // Postcondition:   x == oldof(x) + 1
{
    ++x; // Function body.
}

前置条件声明,在函数进入时,参数 x 必须严格小于其类型的最大允许值(这样才能将其增加 1 而不会溢出)。后置条件声明,在函数退出时,参数 x 必须相对于函数执行之前的 旧值 增加 1(此处用 oldof(x) 表示)。请注意,只有当函数体执行未抛出异常时,才应检查后置条件。

现在,让我们使用此库来编程此函数及其契约(参见 introduction.cpp非成员函数)。

#include <boost/contract.hpp>

void inc(int& x) {
    boost::contract::old_ptr<int> old_x = BOOST_CONTRACT_OLDOF(x); // Old value.
    boost::contract::check c = boost::contract::function()
        .precondition([&] {
            BOOST_CONTRACT_ASSERT(x < std::numeric_limits<int>::max()); // Line 17.
        })
        .postcondition([&] {
            BOOST_CONTRACT_ASSERT(x == *old_x + 1); // Line 20.
        })
    ;

    ++x; // Function body.
}

当调用上述函数 inc 时,此库将:

  • 首先,执行传递给 .precondition(...) 的 functor,该 functor断言 inc 的前置条件。
  • 然后,执行 inc 的主体(即,跟在 boost::contract::check c = ... 声明之后的所有代码)。
  • 最后,执行传递给 .postcondition(...) 的 functor,该 functor断言 inc 的后置条件(除非 inc 的主体抛出了异常)。

例如,如果调用 inc 的代码存在错误,导致函数以 x 等于 std::numeric_limits<int>::max() 的值被调用,那么程序将以类似如下的错误消息终止(并且将清楚地表明错误在调用代码中)。

precondition assertion "x < std::numeric_limits<int>::max()" failed: file "introduction.cpp", line 17

相反,如果在 inc 的实现中存在错误,导致函数体执行后 x 未增加 1,那么程序将以类似如下的错误消息终止(并且将清楚地表明错误在 inc 的主体中):[2]

postcondition assertion "x == *old_x + 1" failed: file "introduction.cpp", line 20

默认情况下,当断言失败时,此库会将一条错误消息(如上所示)打印到标准错误流 std::cerr 并调用 std::terminate 来终止程序(此行为可以自定义为执行任何用户指定的动作,包括抛出异常,参见 抛出异常)。请注意,此库打印的错误消息包含所有必要信息,可以轻松且唯一地识别出契约断言失败的代码位置。[3]

[Note] 注意

要使用此库而不手动编写大量样板代码来编程断言契约的 functor,需要 C++11 lambda 函数(参见 无 Lambda 函数)。即便如此,此库的实现不使用 C++11 功能,应该能在大多数现代 C++ 编译器上工作(参见 入门)。

除了上面示例中所示的非成员函数契约外,此库还允许为构造函数、析构函数和成员函数编程契约。这些可以检查类不变量,还可以子契约,继承和扩展基类的契约(参见 introduction_public.cpp公共函数重写):[4]

template<typename T>
class vector
    #define BASES public pushable<T>
    : BASES
{
public:
    typedef BOOST_CONTRACT_BASE_TYPES(BASES) base_types; // For subcontracting.
    #undef BASES

    void invariant() const { // Checked in AND with base class invariants.
        BOOST_CONTRACT_ASSERT(size() <= capacity());
    }

    virtual void push_back(T const& value,
            boost::contract::virtual_* v = 0) /* override */ { // For virtuals.
        boost::contract::old_ptr<unsigned> old_size =
                BOOST_CONTRACT_OLDOF(v, size()); // Old values for virtuals.
        boost::contract::check c = boost::contract::public_function< // For overrides.
                override_push_back>(v, &vector::push_back, this, value)
            .precondition([&] { // Checked in OR with base preconditions.
                BOOST_CONTRACT_ASSERT(size() < max_size());
            })
            .postcondition([&] { // Checked in AND with base postconditions.
                BOOST_CONTRACT_ASSERT(size() == *old_size + 1);
            })
        ;

        vect_.push_back(value);
    }
    BOOST_CONTRACT_OVERRIDE(push_back) // Define `override_push_back` above.

    // Could program contracts for those as well.
    unsigned size() const { return vect_.size(); }
    unsigned max_size() const { return vect_.max_size(); }
    unsigned capacity() const { return vect_.capacity(); }

private:
    std::vector<T> vect_;
};

语言支持

此库的作者主张将契约添加到核心语言。将契约式编程添加到 C++ 标准比任何库实现都有许多优点(编程契约的语法更短、更简洁,可以在声明而不是定义中指定契约,强制执行契约的常量正确性,预期的编译和运行时间更快,供应商可以开发静态分析工具来在可能的情况下静态识别和检查契约,基于契约条件可以改进编译器优化等)。

P0380 提案支持基本的契约式编程,它已被接受并将包含在 C++20 中。这无疑是朝着正确的方向迈出的一步,但遗憾的是,P0380 仅支持前置条件和后置条件,而缺少类不变量和后置条件中的旧值等重要功能,更不用说缺少子契约等更高级的功能了(诸如 N1962 等更完整的提案已被 C++ 标准委员会拒绝)。相反,此库支持所有契约式编程功能(参见 功能摘要,以详细比较此库支持的功能与不同契约式编程提案中列出的功能,参见 参考文献,以获取在设计和实现此库期间考虑的参考列表,包括提交给 C++ 标准委员会的大多数契约式编程提案)。



[1] Design by Contract (DbC) 是 Eiffel Software 公司的注册商标,它最早由 Eiffel 编程语言引入(参见 [Meyer97])。

[2] 在此示例中,函数体仅由一条简单的指令 ++x 组成,因此通过目视检查很容易确认它不包含任何错误,并且始终将 x 增加 1,因此函数后置条件永远不会失败。在实际生产代码中,函数体很少如此简单,并且可能隐藏错误,因此检查后置条件是有用的。

[3] 理由: 此库打印的断言失败消息的格式类似于 Clang 在 C 风格 assert 宏失败时打印的消息。

[4] 在此示例中,pushable 基类仅用于演示子契约,它有些任意,可能不会出现在实际生产代码中。


Next