Boost C++ 库

……这是世界上评价最高、设计最精巧的 C++ 库项目之一。 Herb SutterAndrei Alexandrescu, 《C++ 编码标准》

第一章. 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++ 编程语言的契约式编程(又称“契约设计”或 DbC)[1]。该库支持所有契约式编程特性:子契约、类不变量(也支持静态和 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