这段摘要讲的是:
.cpp
文件和头文件、库)。大型 C++ 软件要求不仅要正确实现组件,还要通过良好的设计和防御性机制防止客户端误用,否则系统仍会“broken”。
bsls_assert
设施(Bloomberg 的断言机制)本讲座通过DbC(契约式设计)+ 防御式编程 + 明确的接口契约,教你如何构建既高效又健壮的 C++ 库组件,并说明为何适度利用“未定义行为”是一种工程上的权衡与优化。
.h
和 .cpp
分离、命名空间、层次结构等)。bsls_assert
:
bsls_asserttest
组件验证断言是否能在错误用法下触发这个大纲展示了如何从系统设计、接口约定,到实现防御、测试验证,全流程构建健壮、可控、可测的 C++ 组件库。
class Account {
public:
void deposit(double amount);
void withdraw(double amount);
};
.h
/ .hpp
).cpp
).a
, .so
, .dll
)account.h // 声明类 Account
account.cpp // 实现类 Account 的方法
libbanking.a // 编译出的静态库,供其他程序链接使用
特性 | Logical Design | Physical Design |
---|---|---|
本质 | 行为的设计 | 结构和文件的组织方式 |
关注点 | 类、函数、契约 | 文件、模块、编译边界 |
作用范围 | 面向设计者和维护者 | 面向构建、发布与部署 |
示例 | 类 Account 的职责划分 | .h /.cpp 的划分与依赖 |
Account
类负责账户操作逻辑,Transaction
类负责交易逻辑。.h
)、源文件(.cpp
)、模块目录、库文件等。/banking/
├── account.h
├── account.cpp
├── transaction.h
└── transaction.cpp
逻辑设计(类、接口) | 物理实现(文件、路径) |
---|---|
Account 类 |
banking/account.h , account.cpp |
TransactionProcessor 类 |
banking/transaction_processor.cpp |
BankInterface 抽象接口 |
interfaces/bank_interface.h |
总结:逻辑设计是“我们想怎么组织功能”,物理设计是“我们怎么组织代码文件来支持这些功能”。好的物理设计能清晰反映出背后的逻辑结构,从而提升可维护性、可测试性与可扩展性。 |
文件名 | 说明 |
---|---|
component.h |
公共头文件,声明 API,供其他组件使用。 |
component.cpp |
实现文件,实现 .h 中声明的函数。 |
component.t.cpp |
测试驱动程序(test driver),测试该组件。 |
component/
├── component.h // 声明接口
├── component.cpp // 实现接口
└── component.t.cpp // 单元测试(test driver)
.h
文件用于声明 —— 提供接口,不暴露实现。.cpp
文件用于定义 —— 封装实现细节。.t.cpp
文件用于验证 —— 提供组件级测试。这一结构体现了 “组件级开发” 的核心理念,即将逻辑功能模块化,并映射为清晰、可复用、可测试的物理单元。
Shape
、Polygon
、PointList
等)逐步引出模块之间的各种依赖关系类型。关系类型 | 含义说明 |
---|---|
Is-A | 继承关系(如:Polygon 是一个 Shape ) |
Uses-in-the-Interface | 在接口中使用(如:函数参数或返回值中引用某类型) |
Uses-in-the-Implementation | 在内部实现中使用(如:函数内部临时变量引用某类型) |
Uses-in-name-only | 仅仅出现在注释或文档中,编译器无法察觉,但读者可以感知(例如注释中提到某类) |
这些逻辑关系会导致不同组件在 物理构建和链接 上产生依赖。例如:
Polygon
在接口中用到 PointList
,意味着其 .h
头文件需要包含 PointList
;Polygon
在实现中用到 PointList_Link
,那就是实现层面的依赖。通过分析依赖图,我们可以为每个组件打上“层号”,表示它在整个系统中依赖链的深度:
组件 | 层级号(Level) | 说明 |
---|---|---|
Point , PointList_Link |
1 | 基础组件,无依赖 |
PointList |
2 | 依赖 Point 与 PointList_Link |
Polygon |
3 | 依赖 PointList |
Shape |
1 | 无依赖(或者视作抽象基类) |
这种 “层级编号” 有助于: |
逻辑关系 → 物理依赖 → 构建顺序 → 设计质量。
模块间的关系不仅影响代码结构,也深刻影响构建流程和团队协作。越早在设计阶段建立清晰的依赖关系,后期的维护成本就越低。
.cpp
、.h
文件划分,库的组织,编译单元的依赖。Polygon
Is-A Shape
→ 必然在 .h
中包含 Shape.h
→ Polygon
物理上依赖 Shape
。Polygon
的方法参数或返回值包含 PointList
→ 产生接口依赖 → 物理上依赖 PointList
。PointList_Link
),也会导致 .cpp
的依赖。问题 | 核心答案 |
---|---|
逻辑 vs 物理设计 | 抽象结构 vs 实现结构 |
逻辑关系如何导出物理依赖 | Is-A、Uses 关系决定物理文件间的包含和链接 |
Level Numbers 是什么 | 用于衡量依赖深度,指导构建与设计 |
复习组件物理结构设计:
.h
、.cpp
、.t.cpp
文件接口与契约:
什么是好的契约?
如何实现防御性检查:
bsls_assert
组件(Bloomberg 自研的断言库)负向测试(验证契约检查机制):
bsls_asserttest
组件模块 | 内容简介 |
---|---|
1. Physical Design | 理解组件的结构与依赖 |
2. Interfaces & Contracts | 明确前提条件与未定义行为 |
3. Good Contracts | 学会区分并选择 Narrow/Wide Contract |
4. Defensive Checks | 用断言检测并防止错误使用 |
5. Negative Testing | 验证断言机制的可靠性 |
函数声明:
std::ostream& print(std::ostream& stream,
int level = 0,
int spacesPerLevel = 4) const;
stream
。level
:指定缩进等级,负值表示第一行不缩进。spacesPerLevel
:每个缩进等级的空格数,负值表示输出在一行,除了初始缩进外无其它缩进。stream
在调用时无效,函数无任何作用。stream
的引用,方便链式调用。class Date {
// 该类表示一个有效的日期,范围是 0001/01/01 到 9999/12/31。
public:
Date(int year, int month, int day);
// 构造一个有效的日期对象。
// 预条件(contract):传入的 year, month, day 必须表示一个有效日期,
// 即在范围 [0001/01/01 .. 9999/12/31] 内,否则行为未定义。
Date(const Date& original);
// 拷贝构造函数,创建一个值与 original 相同的 Date 对象。
};
public
)等。Date
组件的公共接口包括:class Date
),公开成员函数(构造函数等)bool operator==(const Date& lhs, const Date& rhs);
// 判断两个 Date 对象是否相等(年、月、日都相同返回 true)
bool operator!=(const Date& lhs, const Date& rhs);
// 判断两个 Date 对象是否不等(只要年、月、日有一项不同返回 true)
std::ostream& operator<<(std::ostream& stream, const Date& date);
// 按 "yyyy/mm/dd" 格式把日期写入输出流,并返回该流的引用
Date
组件交互。sqrt
函数作为示例:double sqrt(double value);
// 返回指定 value 的平方根。
// **前置条件**:必须保证 'value >= 0'。
// 否则,行为未定义。
value >= 0
。value
的平方根。条件类型 | 作用对象 | 含义 |
---|---|---|
前置条件 | 对象状态 + 方法输入 | 调用前必须满足,否则行为未定义 |
后置条件 | 对象状态 + 方法输入 | 满足前置条件时,方法必须保证的行为或结果 |
基本行为 (Essential Behavior) | 后置条件的超集 | 还包括性能保证等更广泛的行为规范 |
这部分强调了接口契约不仅仅是函数输入输出的约束,还包括对象状态的一致性和性能保证。 |
类型 | 含义 | 例子/说明 |
---|---|---|
Defined Behavior | 行为被明确规定(specified),实现中必须做到的行为 | 比如 print 方法定义了如何格式化输出,明确说明了参数含义和返回值。 |
Essential Behavior | 核心的、必须保证的行为(是 Defined Behavior 的一部分) | 例如返回流引用、正确格式化输出、保持指定的缩进等。 |
Undefined Behavior | 违反前置条件,行为不确定,可能导致程序崩溃或不可预测的结果 | Date 构造函数传入无效日期时行为未定义。 |
Defined but not Essential Behavior | 明确定义但非核心或非必需保证的行为 | 实现细节依赖或结果的非关键特性,比如输出中额外空白的处理、某些性能细节等。 |
Unspecified and Implementation Dependent | 行为明确但具体实现可以不同,非必需保证 | 输出格式中的某些细节可能因实现不同而不同,但不影响功能。 |
print
方法(Defined & Essential Behavior 示例)std::ostream& print(std::ostream& stream,
int level = 0,
int spacesPerLevel = 4) const;
// 作用:将对象格式化输出到指定的输出流 `stream`。
// 参数说明:
// - level: 缩进级别(负值时,首行不缩进)
// - spacesPerLevel: 每级缩进空格数(负值时,输出为一行,不缩进)
// 返回值:返回传入的流引用。
// 约定:如果输入的 `stream` 无效,操作无效,但不导致错误。
// 行为保证:格式化和缩进符合描述,返回流引用。
// 未定义行为:若调用前 `stream` 状态异常等。
Date
构造函数(Undefined Behavior 示例)Date(int year, int month, int day);
// 创建有效日期,行为未定义除非year/month/day有效且在范围内[0001/01/01 .. 9999/12/31]。
Date
类的对象不变式就是日期必须有效,必须在 0001/01/01
到 9999/12/31
范围内。Date(int year, int month, int day)
的前置条件是传入的年月日必须构成有效日期。Date
对象必须满足不变式,即表示有效日期。class Date {
public:
Date(int year, int month, int day);
Date(const Date& original);
// ...
};
year/month/day
必须是有效日期。“If you give me valid input (including valid state), I will behave如约而至;否则,行为不保证。”
这句话的意思是:
assert
)来检测输入和状态是否合法。定义:
防御式编程是指在代码中加入冗余的运行时检查,用来检测和报告程序中的缺陷(bugs),但不负责“处理”或“隐藏”这些缺陷。它主要是为了在开发或测试阶段尽早发现问题。
strlen(nullptr)
是未定义行为,程序不保证什么结果。strlen(nullptr)
返回0(示例中假设)。strlen(const char *s)
:// 窄契约:
// 假设 s 不能为空,调用者负责保证
size_t strlen(const char *s) {
assert(s); // 只在调试模式下检查
// 计算长度...
}
// 宽契约:
// 允许 s 为 nullptr,返回0表示空字符串
size_t strlen(const char *s) {
if (!s) return 0;
// 计算长度...
}
这里宽契约容易掩盖错误,比如用户本来不应该传 nullptr,传了却没报错。
vector::operator[]
是窄契约,未检查越界,行为未定义。vector::at()
是宽契约,检查越界,抛出异常。以 vector
举例:
idx
超出范围,行为是未定义的。vector::at(int idx)
是宽契约,检查越界,越界时抛异常,定义行为更明确。以 void insert(int idx, const TYPE& value);
举例:
idx < 0
或 idx > length()
时,应该怎么办?if (idx < 0) idx = 0;
if (idx > length()) idx = length();
甚至用模运算循环索引,但这属于宽契约。void insert(int idx, const TYPE& value) {
assert(0 <= idx && idx <= length());
// ...
}
举 replace(int index, const TYPE& value, int numElements);
例子:
index == length()
并且 numElements == 0
,这其实是合理的调用,代表不做任何替换(插入或删除0个元素)。设计原则 | 含义 | 例子 |
---|---|---|
窄契约 (Narrow) | 明确断言,行为未定义时直接崩溃,调用者负责保证参数合法 | operator[] ,insert 参数越界断言失败 |
宽契约 (Wide) | 函数内部处理错误输入,返回状态或修正参数,代码更健壮但可能掩盖错误 | at() 越界抛异常,strlen(nullptr) 返回0 |
适度窄契约 (Appropriately Narrow) | 对合理的边界值允许行为,保持契约的实用性和健壮性 | replace(index == length(), numElements == 0) 允许不报错 |
std::bad_alloc
是库中唯一应该抛出的异常(特别是分配器相关的组件)。abort()
是合理的替代abort()
终止程序是一个可接受的选择,确保不会以错误状态继续执行。std::strlen(0)
:是否应该对传入空指针进行合理处理?Date::setDate(int, int, int)
:是否应该返回状态码告诉调用者设置是否成功?operator[](int index)
是否应该检查 index 是否越界(小于0或大于长度)?insert(int index, const TYPE& value)
函数在 index 超出合法范围时行为应如何?replace(int index, const TYPE& value, int numElements)
在 index 等于长度且 numElements 为零时,行为应否定义?这些问题的核心都在于接口契约的设计——应该严格还是宽松?未定义行为的处理方式?如何平衡易用性和安全性?
选项包括:
需要考虑的因素:
bsls_assert
组件实现防御性检查(Defensive Checks)的详细计划和示例,重点在于如何通过宏和失败处理器来检测和处理客户端误用库代码的情况。以下是对内容的理解和总结:BSLS_ASSERT*
宏,用于在不同场景下进行防御性检查。-DBDE_BUILD_TARGET_OPT
,则禁用(优化模式)。-DBDE_BUILD_TARGET_SAFE
,则启用(安全模式)。-DBDE_BUILD_TARGET_SAFE
模式下激活。typedef void (*Handler)(const char *text, const char *file, int line);
text
)、文件名(file
)和行号(line
)。stderr
,然后终止程序(调用 abort()
)。std::logic_error
中并抛出异常。failSpin
):
stderr
,然后进入循环/休眠状态(等待调试器介入)。factorial
。our_mathutil.h
和 our_mathutil.cpp
):// our_mathutil.h
struct MathUtil {
static double factorial(int n);
// 返回从 1 到 n 的乘积,若 n 为 0 则返回 1。
// 要求:0 <= n <= 100,否则行为未定义。
};
// our_mathutil.cpp
#include
#include
double MathUtil::factorial(int n) {
BSLS_ASSERT(0 <= n); // 检查 n 是否非负
BSLS_ASSERT(n <= 100); // 检查 n 是否不超过 100
// 阶乘实现代码
}
my_client.cpp
):#include
#include
void someFunction() {
double z = our::MathUtil::factorial(-5); // 误用:n < 0
}
bsls_assert.o
。Assertion failed: 0 <= n, file our_mathutil.cpp, line 365
Abort (core dumped)
BSLS_ASSERT
,默认失败处理器为 failAbort
,因此程序在检测到 n < 0
时打印错误并终止。Square
,设置宽度。our_square.h
):#include
class Square {
double d_width;
public:
void setWidth(double width);
// 要求:width >= 0.0,否则行为未定义。
};
inline void Square::setWidth(double width) {
BSLS_ASSERT_SAFE(0.0 <= width); // 检查宽度非负
d_width = width;
}
my_client.cpp
):#include
#include
void someFunction() {
our::Square s;
s.setWidth(-3.14); // 误用:width < 0
}
-DBDE_BUILD_TARGET_SAFE
(否则 BSLS_ASSERT_SAFE
不生效)。Assertion failed: 0 <= width, file our_square.h, line 142
Abort (core dumped)
BSLS_ASSERT_SAFE
,适合内联函数(inline functions),因为这种检查可能开销较高,只有在安全模式下才会触发。Kpoint
。our_kpoint.h
):class Kpoint {
short int d_x, d_y;
public:
Kpoint(int x, int y);
// 要求:-1000 <= x, y <= 1000,否则行为未定义。
};
inline Kpoint::Kpoint(int x, int y) : d_x(x), d_y(y) {
BSLS_ASSERT_SAFE(-1000 <= x); BSLS_ASSERT_SAFE(x <= 1000);
BSLS_ASSERT_SAFE(-1000 <= y); BSLS_ASSERT_SAFE(y <= 1000);
}
BSLS_ASSERT_SAFE
,因为构造函数是内联的,检查条件可能较昂贵,符合指南建议。HashTable
,调整大小。our_hashtable.h
和 our_hashtable.cpp
):// our_hashtable.h
class HashTable {
public:
void resize(double loadFactor);
// 调整哈希表大小,要求:loadFactor > 0,否则行为未定义。
};
// our_hashtable.cpp
void HashTable::resize(double loadFactor) {
BSLS_ASSERT(0 < loadFactor); // 检查 loadFactor 正数
// 实现代码
}
BSLS_ASSERT
,因为 resize
不是内联函数,且检查成本较低,符合指南建议。TradingSystem
。our_tradingsystem.h
和 our_tradingsystem.cpp
):// our_tradingsystem.h
class TradingSystem {
public:
void executeTrade(int scalingFactor);
// 要求:scalingFactor >= 0 且能被 100 整除,否则行为未定义。
};
// our_tradingsystem.cpp
void TradingSystem::executeTrade(int scalingFactor) {
BSLS_ASSERT_OPT(0 <= scalingFactor); // 检查非负
BSLS_ASSERT_OPT(0 == scalingFactor % 100); // 检查整除
// 实现代码
}
failSpin
):int main(int argc, const char *argv[]) {
bsls_Assert::setFailureHandler(&bsls_Assert::failSpin); // 设置失败处理器
return 0;
}
void clientFunction(our::TradingSystem *objectPtr) {
objectPtr->executeTrade(10022); // 误用:10022 不能被 100 整除
}
-DBDE_BUILD_TARGET_OPT
(但 BSLS_ASSERT_OPT
始终生效)。(printed message sent to console room, waiting…)
BSLS_ASSERT_OPT
,因为条件对系统至关重要(critical),且开销极低。自定义了失败处理器为 failSpin
,程序会暂停并等待调试。bsls_assert
组件通过三种宏(BSLS_ASSERT_OPT
、BSLS_ASSERT
、BSLS_ASSERT_SAFE
)提供分级防御性检查,允许开发者根据性能需求选择合适的检查级别。failAbort
、failThrow
、failSleep/failSpin
)提供了灵活的错误处理机制,支持终止、抛异常或调试。bsls_assert
组件来处理这些情况。以下是对内容的理解和总结:bsl::strlen
函数bsl::size_t bsl::strlen(const char *string)
// 要求:string 必须以 null 终止,否则行为未定义。
{
BSLS_ASSERT_SAFE(string); // 断言检查
const char *p = string;
while (*p) { ++p; }
return p - string;
}
nullptr
(0),BSLS_ASSERT_SAFE
会触发,因为 string
未满足 null 终止条件。这被归类为 Hard UB,因为它可能导致内存访问错误或崩溃。Account::setName
函数class Account {
std::string d_name;
public:
void setName(const char *name)
// 要求:name 必须以 null 终止,否则行为未定义。
{
BSLS_ASSERT_SAFE(name); // 断言检查
d_name = name;
}
};
nullptr
触发 BSLS_ASSERT_SAFE
,被归类为 Hard UB,因为它可能导致无效内存操作。badLibraryFunction
vs. goodLibraryFunction
void badLibraryFunction(std::string& result, ...)
{
BSLS_ASSERT_SAFE(&result); // 太晚了!
// ...
}
int someClientFunction(std::string *resultId, ...)
{
// 要求:resultId 必须非空
badLibraryFunction(*resultId, ...);
}
BSLS_ASSERT_SAFE
检查太晚(在解引用 resultId
之后),如果 resultId
为 nullptr
,会导致 Hard UB(解引用空指针)。void goodLibraryFunction(std::string *result, ...)
{
BSLS_ASSERT_SAFE(result); // 及时检查
// ...
}
int someClientFunction(std::string *resultId, ...)
{
// 要求:resultId 必须非空
goodLibraryFunction(resultId, ...);
}
result
,如果 resultId
为 nullptr
,触发 BSLS_ASSERT_SAFE
,保持为 Soft UB(契约违反),避免 Hard UB。failThrow
失败处理器int main(int argc, const char *argv[]) {
bsls_Assert::setFailureHandler(&bsls_Assert::failThrow); // 设置抛出异常
// 设置保存内部状态的逻辑
try {
// 所有有用工作在这里
} catch (const std::logic_error& exception) {
// 保存内部状态并退出
}
return 0;
}
failThrow
处理器,当断言失败时抛出 std::logic_error
异常。try-catch
块捕获异常,允许在异常发生时保存状态并优雅退出。BSLS_ASSERT_OPT
→ BSLS_ASSERT_OPT_IS_ACTIVE
BSLS_ASSERT
→ BSLS_ASSERT_IS_ACTIVE
BSLS_ASSERT_SAFE
→ BSLS_ASSERT_SAFE_IS_ACTIVE
BSLS_ASSERT*_IS_ACTIVE
MyDate
类):class MyDate {
int d_serialDate; // 有效范围 [1 .. 3652061]
public:
MyDate();
// 创建值为 '0001Jan01' 的对象
#if defined(BSLS_ASSERT_SAFE_IS_ACTIVE)
~MyDate(); // 仅在安全模式下定义析构函数
#endif
};
inline MyDate::MyDate() : d_serialDate(1) { } // 0001Jan01
#if defined(BSLS_ASSERT_SAFE_IS_ACTIVE)
inline MyDate::~MyDate() {
BSLS_ASSERT_SAFE(1 <= d_serialDate); // 检查下限
BSLS_ASSERT_SAFE(d_serialDate <= 3652061); // 检查上限
}
#endif
BSLS_ASSERT_SAFE_IS_ACTIVE
定义时启用。d_serialDate
的有效性,优化模式下省略检查以提升性能。BSLS_ASSERT*
宏和及时的检查,可以将 Hard UB 风险降至最低。failThrow
等)和条件编译支持,允许开发者根据需求调整行为和性能开销。goodLibraryFunction
)是关键,晚检查(如 badLibraryFunction
)可能导致 Hard UB。BSLS_ASSERT*_IS_ACTIVE
)支持在不同构建目标间切换检查。BSLS_ASSERT_OPT
和 BSLS_ASSERT
,而库可能只启用 BSLS_ASSERT_OPT
。BSLS_ASSERT_OPT
、BSLS_ASSERT
、BSLS_ASSERT_SAFE
)必须设计为 ABI 兼容。bsls_assert
(如内联函数或函数模板中)可能导致 ODR 违反。.cpp
文件中BSLS_ASSERT_OPT
和 BSLS_ASSERT
。BSLS_ASSERT_OPT
。
BSLS_ASSERT
在库中未激活,而在客户端中激活,导致行为不一致。BSLS_ASSERT_OPT
和 BSLS_ASSERT
。
BSLS_ASSERT
在库和客户端中都激活,行为一致。.cpp
文件中,ODR 问题不明显,因为每个编译单元有独立的定义。但断言行为的不一致可能导致运行时问题。.h
文件中BSLS_ASSERT_OPT
和 BSLS_ASSERT
。BSLS_ASSERT_OPT
、BSLS_ASSERT
和 BSLS_ASSERT_SAFE
。
BSLS_ASSERT_SAFE
在客户端中可能未激活,而在库中激活,导致内联函数的定义不一致(违反 ODR)。BSLS_ASSERT_SAFE
的检查,而库期望执行该检查。.h
文件中BSLS_ASSERT_OPT
和 BSLS_ASSERT
。BSLS_ASSERT_OPT
。
BSLS_ASSERT
在客户端中激活,但在库中未激活,导致函数模板实例化的定义不一致(违反 ODR)。BSLS_ASSERT_OPT
、BSLS_ASSERT
、BSLS_ASSERT_SAFE
)编译。bsls_assert
(如内联函数或函数模板)可能导致 ODR 违反,因为不同编译单元可能看到不一致的定义。.cpp
文件中的定义:影响较小,仅表现为断言行为不一致。BSLS_ASSERT_SAFE
),以减少 ODR 违反的风险。main.cpp
#include
#include // 为了使用 std::printf
static bool globalEnableOurPrintingFlag = true;
static void ourFailureHandler(const char *text, const char *file, int line)
// 打印指定的表达式 'text'、文件名 'file' 和行号 'line' 到 stdout,格式为逗号分隔列表,
// 将空字符串参数替换为空字符串(除非 globalEnableOurPrintingFlag 为 false 禁用打印);
// 然后无条件终止进程。
{
if (!text) text = ""; // 防止空指针,替换为 ""
if (!file) file = ""; // 防止空指针,替换为 ""
if (globalEnableOurPrintingFlag) {
std::printf("%s, %s, %d\n", text, file, line); // 打印信息
}
std::abort(); // 终止进程
}
ourFailureHandler
是一个自定义失败处理器,符合 bsls_assert
组件的回调签名:void (*Handler)(const char*, const char*, int)
。text
和 file
是否为 nullptr
,如果是则替换为空字符串。globalEnableOurPrintingFlag
决定是否打印断言失败信息(格式:text, file, line
)。std::abort()
终止程序。int main(int argc, const char *argv[])
{
bsls_Assert::Handler f = &::ourFailureHandler; // 获取函数指针
bsls_Assert::setFailureHandler(f); // 设置自定义失败处理器
bsls_Assert::invokeHandler("str1", "str2", 3); // 手动调用处理器
return 0;
}
$ CC -o a.out main.cpp
$ ./a.out
str1, str2, 3
Abort (core dumped)
bsls_Assert::Handler
是一个函数指针类型,用于存储失败处理器的地址。bsls_Assert::setFailureHandler
设置自定义处理器 ourFailureHandler
。bsls_Assert::invokeHandler
手动调用处理器,传入测试参数 "str1"
、"str2"
和 3
。str1, str2, 3
,然后程序因 std::abort()
终止。-DBDE_BUILD_TARGET_OPT
,则禁用(优化模式)。-DBDE_BUILD_TARGET_SAFE
,则启用(安全模式)。-DBDE_BUILD_TARGET_SAFE
模式下激活。BDE_BUILD_TARGET_*
标志决定)。-D BSLS_ASSERT_LEVEL_NONE
:
BSLS_ASSERT_OPT
、BSLS_ASSERT
、BSLS_ASSERT_SAFE
)都不激活。-D BSLS_ASSERT_LEVEL_ASSERT_OPT
:
BSLS_ASSERT_OPT
激活,其他宏(BSLS_ASSERT
和 BSLS_ASSERT_SAFE
)不激活。-D BSLS_ASSERT_LEVEL_ASSERT
:
BSLS_ASSERT_OPT
和 BSLS_ASSERT
激活,但 BSLS_ASSERT_SAFE
不激活。-D BSLS_ASSERT_LEVEL_ASSERT_SAFE
:
BSLS_ASSERT_OPT
、BSLS_ASSERT
、BSLS_ASSERT_SAFE
)都激活。CC -DBDE_BUILD_TARGET_OPT -DBSLS_ASSERT_LEVEL_ASSERT_SAFE ...
-DBDE_BUILD_TARGET_OPT
通常会禁用 BSLS_ASSERT
,但被 -DBSLS_ASSERT_LEVEL_ASSERT_SAFE
覆盖。BSLS_ASSERT_SAFE
)都激活,即使在优化模式下。bsls_assert
组件创建和使用自定义失败处理器 ourFailureHandler
。bsls_Assert::setFailureHandler
安装处理器,bsls_Assert::invokeHandler
测试其行为。BSLS_ASSERT*
宏(BSLS_ASSERT_OPT
、BSLS_ASSERT
、BSLS_ASSERT_SAFE
)根据性能开销分级,开发者可根据构建目标选择激活哪些宏。BSLS_ASSERT_LEVEL_*
),允许开发者通过构建命令行精确控制断言行为。BDE_BUILD_TARGET_*
设置,例如在优化构建中强制启用所有断言。BDE_BUILD_TARGET_SAFE_2
模式和 bsls_assert
组件处理这些情况。以下是对内容的理解和总结:d_backPointers
链表)会改变容器和迭代器的内存布局(memory layout),从而改变类的 ABI(Application Binary Interface)。这意味着以不同模式编译的代码(启用检查 vs. 不启用检查)无法在二进制级别兼容。BDE_BUILD_TARGET_SAFE_2
处理二进制不兼容检查my_string.h
):class String {
char *d_array_p; // 字符数组
std::size_t d_length; // 字符串长度
std::size_t d_capacity; // 容量
bslma_Allocator *d_allocator_p; // 内存分配器
#if defined(BDE_BUILD_TARGET_SAFE_2)
bsl::list<iterator *> d_backPointers; // 反向指针链表
#endif
void removeAll() {
#if defined(BDE_BUILD_TARGET_SAFE_2)
// 使用 d_backPointers 使所有迭代器失效
#endif
d_length = 0; // 清空字符串
}
};
#if defined(BDE_BUILD_TARGET_SAFE_2)
控制是否启用二进制不兼容的检查。BDE_BUILD_TARGET_SAFE_2
模式下,String
类会添加 d_backPointers
成员,用于跟踪所有迭代器。removeAll
方法在启用 SAFE_2
模式时会使用 d_backPointers
使所有迭代器失效,避免未定义行为。BDE_BUILD_TARGET_SAFE_2
未定义,d_backPointers
不存在,String
类的内存布局不同,导致二进制不兼容。BDE_BUILD_TARGET_SAFE
)中启用那些最坏情况时间复杂度更高的检查(例如从 O(log n) 增加到 O(n))。BDE_BUILD_TARGET_SAFE_2
)中。BDE_BUILD_TARGET_SAFE_2
处理高开销检查my_algorithmutil.h
和 my_algorithmutil.cpp
):my_algorithmutil.h
):#include
struct AlgorithmUtil {
static bool isSorted(const std::vector<int>& a);
// 检查向量是否已排序
static bool isMember(const std::vector<int>& a, int v);
// 检查 v 是否在向量 a 中
// 要求:a 必须已排序,否则行为未定义
};
my_algorithmutil.cpp
):bool AlgorithmUtil::isMember(const std::vector<T>& a, int v) {
#if defined(BDE_BUILD_TARGET_SAFE_2)
BSLS_ASSERT_SAFE(isSorted(a)); // 检查向量是否已排序
#endif
// 实现二分查找逻辑(省略)
}
isSorted(a)
的时间复杂度为 O(n),远高于二分查找的 O(log n)。BDE_BUILD_TARGET_SAFE_2
模式下启用此检查,避免在普通安全模式下影响性能。isMember
方法要求输入向量已排序,否则行为未定义。通过 BSLS_ASSERT_SAFE
强制检查此条件。-DBDE_BUILD_TARGET_SAFE_2
构建。
SAFE_2
模式可能引入二进制不兼容的更改(如 String
类的 d_backPointers
),需要一致性。SAFE_2
模式下,所有断言级别(BSLS_ASSERT_OPT
、BSLS_ASSERT
、BSLS_ASSERT_SAFE
)默认启用。BSLS_ASSERT_LEVEL_*
覆盖默认断言级别,例如:CC -DBDE_BUILD_TARGET_SAFE_2 -DBSLS_ASSERT_LEVEL_ASSERT myalgorithms.cpp ...
SAFE_2
模式下,启用高开销检查,但通过 -DBSLS_ASSERT_LEVEL_ASSERT
限制断言级别,仅激活 BSLS_ASSERT_OPT
和 BSLS_ASSERT
,禁用 BSLS_ASSERT_SAFE
。DEFAULT
:默认模式(未指定其他目标)。-DBDE_BUILD_TARGET_OPT
:优化模式。-DBDE_BUILD_TARGET_SAFE
:安全模式。-DBDE_BUILD_TARGET_SAFE_2
:更高级安全模式(支持二进制不兼容和高开销检查)。-DBSLS_ASSERT_LEVEL_NONE
:禁用所有断言。-DBSLS_ASSERT_LEVEL_ASSERT_OPT
:仅启用 BSLS_ASSERT_OPT
。-DBSLS_ASSERT_LEVEL_ASSERT
:启用 BSLS_ASSERT_OPT
和 BSLS_ASSERT
。-DBSLS_ASSERT_LEVEL_ASSERT_SAFE
:启用所有断言(包括 BSLS_ASSERT_SAFE
)。BDE_BUILD_TARGET_SAFE_2
模式控制此类检查,确保启用时整个程序一致构建。String
类在 SAFE_2
模式下添加 d_backPointers
,用于使迭代器失效。BDE_BUILD_TARGET_SAFE_2
模式,仅在需要时启用此类检查。AlgorithmUtil::isMember
方法在 SAFE_2
模式下检查输入向量是否已排序。BDE_BUILD_TARGET_SAFE_2
专门用于处理二进制不兼容和高开销的检查,默认启用所有断言级别。BSLS_ASSERT_LEVEL_*
覆盖调整断言级别,平衡安全性和性能。BDE_BUILD_TARGET_SAFE
)用于常规检查。BDE_BUILD_TARGET_SAFE_2
)用于更彻底但成本更高的检查。bsls_assert
宏、配置策略、编译目标等核心问题。下面是对你列出的问题的逐一解释和理解总结:main
函数;bsls_assert
宏是否启用,在不同模式(如 SAFE
, OPT
, DEBUG
)下行为不同;BSLS_ASSERT_SAFE
vs BSLS_ASSERT
vs BSLS_ASSERT_OPT
?宏 | 使用时机 | 检查级别 | 是否影响性能敏感路径? |
---|---|---|---|
BSLS_ASSERT_SAFE |
开发初期、调试期间,检查非关键路径 | 最严 | 可能较慢 |
BSLS_ASSERT |
一般开发时默认启用,适中强度 | 中等 | 可接受的成本 |
BSLS_ASSERT_OPT |
性能敏感代码、发布版本中保留少量关键检查 | 最轻 | 通常很快 |
SAFE
做复杂校验;ASSERT
保证一般契约;OPT
检查基本但致命的错误。bsls_assert
的定义在不同构建目标中有不同实现;SAFE
,另一个是 OPT
);bsls_assert
是一个调试辅助工具;BDE_BUILD_TARGET_SAFE_2
?它与 BDE_BUILD_TARGET_SAFE
有何关系与区别?BDE_BUILD_TARGET_SAFE_2
的作用:
BSLS_ASSERT_SAFE
时避免冲突;BDE_BUILD_TARGET_SAFE
的关系:
SAFE_2
是 SAFE
的扩展或替代,解决其在大规模构建系统中的ODR冲突问题;SAFE_2
可以使某些宏逻辑在 header 中同时兼容多个目标。核心概念 | 要点 |
---|---|
谁决策防御性检查? | 应用拥有者(因为性能与行为取舍) |
为什么不在函数契约中承诺断言行为? | 行为取决于构建配置,非固定的一部分 |
不同断言宏的使用时机? | 根据调试深度与性能要求灵活使用 |
硬/软未定义行为区别? | 硬UB不可恢复,软UB可检测处理 |
为什么允许 ODR 违规? | 宏行为不影响接口语义,是调试用途 |
SAFE vs SAFE_2 | SAFE_2 是为避免 ODR 问题而引入的新机制 |
main()
函数的 C++ 文件;component.t.cpp
;case n:
的形式出现;1
开始,一直到 n
;ASSERT(...)
断言。int main(int argc, char *argv[]) {
int test = argc > 1 ? atoi(argv[1]) : DEFAULT_TEST;
switch (test) {
case 1: {
// Test case 1
ASSERT(...);
...
} break;
case 2: {
// Test case 2
...
} break;
...
default: {
printf("WARNING: TEST CASE %d NOT FOUND.\n", test);
return -1;
}
}
return 0;
}
testDriver [ testCase# [ addlArgs ... ] ]
testCase#
,只执行那个用例;返回值 | 含义 |
---|---|
0 |
成功,所有断言通过 |
n > 0 |
有 n 个断言失败 |
< 0 |
测试用例编号无效(not found) |
component.t.cpp(42): 2 == sqrt(4) (failed)
./component.t.exe 3 verbose
Testing length 0
without aliasing
with aliasing
Testing length 1
...
方面 | 要点 |
---|---|
核心文件 | component.t.cpp ,定义 main() |
测试结构 | switch + 连续编号 case |
断言方式 | 使用 ASSERT(...) 检查行为 |
命令行接口 | testDriver [ testCase# [ args... ] ] |
返回值语义 | 0=成功 ,n=断言失败数 ,负值=错误 |
用户体验 | 安静成功、有错提示、支持 verbose 输出 |
用途 | 初始开发 + 回归测试兼顾 |
一个典型的测试驱动 (component.t.cpp
) 包括以下结构:
#include
指令#include
)#include
, #include
等// [ 2] Point(int x, int y)
// [ 1] void setX(int x)
// [ 1] int y() const
// [ 4] void moveBy(int dx, int dy)
// [ 3] void moveTo(int x, int y)
ASSERT
宏定义verbose
变量支持调试int main(int argc, char *argv[]) {
int test = argc > 1 ? atoi(argv[1]) : DEFAULT_TEST;
bool verbose = argc > 2;
switch (test) {
case 1: {
// TEST CASE 1
} break;
case 2: {
// TEST CASE 2
} break;
// ...
default: {
cout << "WARNING: TEST CASE " << test << " NOT FOUND.\n";
return -1;
}
}
return 0;
}
每个 case N
中,按照以下规范书写测试内容:
case 2: {
//-------------------------------------------------------
// UNIQUE BIRTHDAY
// The value returned for an input of 365 is small.
//
// Concerns:
//: 1 That it can represent the result as a 'double'.
//: 2 That it returns decreasing values for increasing input.
//: 6 That the special-case input of 0 returns 1.
//
// Plan:
// Test explicit values near 0, 365, and INT_MAX.
//
// Testing:
// double uniqueBirthday(int value);
//-------------------------------------------------------
if (verbose) cout << "\nUNIQUE BIRTHDAY\n===============\n";
ASSERT(1 == uniqueBirthday(0)); // Concern 6
ASSERT(1 == uniqueBirthday(1)); // Value near zero
ASSERT(1 > uniqueBirthday(2)); // Decreasing behavior
ASSERT(0 < uniqueBirthday(365)); // Value of interest
ASSERT(0 == uniqueBirthday(366)); // Boundary test
} break;
部分 | 内容与作用 |
---|---|
TITLE | 简短的标签,比如 UNIQUE BIRTHDAY ,用于 verbose 输出 |
CONCERNS | 明确列出测试这个函数可能出错的点(负向测试关注点) |
PLAN | 如何用实际代码检测上述 concern,强调方法论而非细节 |
TESTING | 与 test plan 呼应,列出当前测试的是哪个函数 |
测试代码 | 使用 ASSERT(...) 检查结果,确保覆盖所有 concern |
BSLS_ASSERT
。区域 | 内容 |
---|---|
Include | 所有依赖头文件 |
Test Plan | 用 [N] 标记函数与测试用例关系 |
Test Apparatus | 定义 ASSERT 、通用变量如 verbose |
main() 函数 |
解析测试编号,进入 switch 调用对应测试用例 |
每个测试用例 | 有 Title、Concerns、Plan、Testing 注释块,配合断言验证逻辑 |
返回值 | 0 成功,正数=断言失败数,负数=未找到测试编号 |
定义:
Negative Testing 是一种验证“未在文档中公开声明的防御性检查”是否在预期的 assertion-level build modes 中正确起作用的测试方式。
负向测试的目标是验证你写在库内部用来“防止用户滥用”的那些 BSLS_ASSERT/BSLS_ASSERT_SAFE/BSLS_ASSERT_OPT 是否 真的能在你预期的构建模式下触发并起作用。
这部分你提到了一些“看起来傻”的问题,但其实是很有深度的讨论:
因为:
你提供的三点回答非常重要:
nullptr
但函数内部没有处理。BSLS_ASSERT
等宏 主动插入防御性检查,所以行为是“已知”的(断言失败)。项目 | 内容 |
---|---|
目的 | 确保防御性断言(如 BSLS_ASSERT )能在 debug/safe 构建中被正确触发 |
测试什么 | 违反 precondition 的调用是否触发断言(例如传非法参数) |
为什么重要 | 错误检测机制往往在运行时才被用到,必须在开发阶段被验证 |
可以测试吗? | 可以!因为我们知道断言具体位置(白盒) |
重点 | Soft undefined behavior 是测试的重点对象 |
void setSize(int size) {
BSLS_ASSERT(size > 0); // Defensive check
d_size = size;
}
测试代码:
if (veryVerbose) cout << "Testing setSize precondition failure\n";
// Negative test - deliberately violate precondition
ASSERT_SAFE_FAIL(obj.setSize(0)); // This should trigger BSLS_ASSERT_SAFE
ASSERT_SAFE_PASS(obj.setSize(1)); // Valid case
Negative Testing 是对库质量的关键保障机制。它不是“非正常测试”,而是有策略、有目的地 验证程序在遭遇非法输入时是否以可预期的方式失败(fail-fast)。这在金融系统、航空、医疗等高可靠性系统中至关重要。
bsls_asserttest
组件来实现它。以下是对你提供内容的全面整理和理解,方便你更系统地掌握这个主题。必须能够调用一个超出函数契约(contract)的用法,并能观察到这个误用被捕获了。
Negative testing 必须非常容易在任何测试环境(尤其是我们自己的)中实现。
Negative tests 必须容易并高效地运行。
Contract violation 必须被当前测试的组件所捕获。
不要在防御性断言不启用的构建模式下执行负向测试。
BSLS_ASSERT_SAFE_IS_ACTIVE
时运行某些断言测试。bsls_asserttest
)创建一个专门用于测试的断言失败处理器(test handler),它抛出一个特制的异常,该异常记录断言触发的详细信息:
字段 | 描述 |
---|---|
a. |
组件文件名 |
b. |
源码行号 |
c. |
断言失败的表达式 |
d. |
断言的级别(如 SAFE / ASSERT / OPT) |
这是为了让测试代码能准确判断是不是预期的断言在预期位置触发。 |
bsls_asserttest
提供了一套宏和工具函数(如 BSLS_ASSERTTEST_ASSERT_SAFE_FAIL
)用来简化负向测试。
#include
#include
void setSize(int size) {
BSLS_ASSERT_SAFE(size > 0);
// ...
}
int main() {
using namespace bsls;
// 设置测试断言处理器
AssertTest::setFailureHandler(AssertTest::failTestDriver);
// 开始负向测试
if (BSLS_ASSERT_SAFE_IS_ACTIVE) {
ASSERT(AssertTest::tryFailure(BSLS_ASSERTTEST_ASSERT_SAFE_FAIL(setSize(0))));
ASSERT(AssertTest::tryPass (BSLS_ASSERTTEST_ASSERT_SAFE_PASS(setSize(5))));
}
return 0;
}
关键点 | 解释 |
---|---|
测试目的 | 验证内部断言机制能如预期那样“在需要时阻止错误调用” |
测试方式 | 使用白盒方式,故意违反 precondition 并确认断言被触发 |
工具支持 | bsls_asserttest 提供了标准宏、辅助类、失败处理机制 |
注意事项 | 不要在断言未激活的构建模式中做违规测试,防止硬崩溃 |
要求 | 实现要简单、运行要集中高效、异常要精确捕获信息 |
这段内容讨论了负向测试(Negative Testing),即通过 bsls_asserttest 组件测试函数在违反其契约(narrow contract)时的行为,验证防御性检查是否按预期触发。以下是对内容的理解和总结: |
ASSERT
测试宏,用于处理负向测试中的错误。bsls_Assert::setFailureHandler
设置 bsls_AssertTest::failTestDriver
作为失败处理器。factorial(n)
函数factorial(n)
要求 0 <= n <= 100
,否则行为未定义。bsls_Assert::setFailureHandler(&bsls_AssertTest::failTestDriver);
BSLS_ASSERTTEST_ASSERT_FAIL(factorial(-1)); // 超出下界,期望失败
BSLS_ASSERTTEST_ASSERT_PASS(factorial(0)); // 边界值,期望通过
BSLS_ASSERTTEST_ASSERT_PASS(factorial(100)); // 边界值,期望通过
BSLS_ASSERTTEST_ASSERT_FAIL(factorial(101)); // 超出上界,期望失败
bsls_AssertTest::failTestDriver
是一个专门用于测试的失败处理器,会抛出 bsls_AssertTestException
异常,而不是终止程序。factorial(-1)
:n < 0
,违反契约,期望断言触发(BSLS_ASSERTTEST_ASSERT_FAIL
)。factorial(0)
:n = 0
,符合契约,期望断言不触发(BSLS_ASSERTTEST_ASSERT_PASS
)。factorial(100)
:n = 100
,符合契约,期望断言不触发(BSLS_ASSERTTEST_ASSERT_PASS
)。factorial(101)
:n > 100
,违反契约,期望断言触发(BSLS_ASSERTTEST_ASSERT_FAIL
)。BSLS_ASSERTTEST_ASSERT_FAIL
和 BSLS_ASSERTTEST_ASSERT_PASS
BSLS_ASSERTTEST_ASSERT_FAIL(factorial(-1))
为例):#if BSLS_ASSERT_IS_ACTIVE
try {
factorial(-1);
ASSERT(false); // 如果未触发断言,则测试失败
} catch(const bsls_AssertTestException& e) {
ASSERT(0 == strcmp("our_mathutil.cpp", e.filename())); // 验证断言来源文件
}
#endif
BSLS_ASSERT_IS_ACTIVE
启用时执行(即 BSLS_ASSERT
宏处于激活状态)。factorial(-1)
,期望触发断言。bsls_AssertTest::failTestDriver
抛出 bsls_AssertTestException
。our_mathutil.cpp
)。factorial(-1)
没有失败),执行到 ASSERT(false)
,测试失败。bsls_Assert::setFailureHandler(&bsls_AssertTest::failTestDriver);
#if BSLS_ASSERT_IS_ACTIVE
try {
factorial(-1);
ASSERT(false); // 应触发断言,不应到达这里
} catch(const bsls_AssertTestException& e) {
ASSERT(0 == strcmp("our_mathutil.cpp", e.filename()));
}
#endif
factorial(0); // 期望通过,无需额外检查
factorial(100); // 期望通过,无需额外检查
#if BSLS_ASSERT_IS_ACTIVE
try {
factorial(101);
ASSERT(false); // 应触发断言,不应到达这里
} catch(const bsls_AssertTestException& e) {
ASSERT(0 == strcmp("our_mathutil.cpp", e.filename()));
}
#endif
BSLS_ASSERTTEST_ASSERT_FAIL
):
factorial(-1)
和 factorial(101)
违反契约,期望触发断言。try-catch
捕获 bsls_AssertTestException
,验证断言是否按预期触发。BSLS_ASSERTTEST_ASSERT_PASS
):
factorial(0)
和 factorial(100)
符合契约,调用时不期望触发断言。try-catch
中(未明确展开,但逻辑上不需要检查异常)。bsls_asserttest
组件验证了断言的正确性,确保函数在违反契约时按预期触发断言。BSLS_ASSERT
)按预期工作。factorial
的 n < 0
和 n > 100
),验证断言触发。bsls_AssertTest::failTestDriver
处理器,捕获断言失败的异常。BSLS_ASSERTTEST_ASSERT_FAIL
和 BSLS_ASSERTTEST_ASSERT_PASS
宏,分别验证断言触发和不触发的情况。BSLS_ASSERTTEST_ASSERT_FAIL
:期望断言触发,捕获异常并验证。BSLS_ASSERTTEST_ASSERT_PASS
:期望断言不触发,直接调用函数。bsls_asserttest
组件提供了专门的工具(宏和失败处理器),简化了负向测试的实现。BSLS_ASSERT_IS_ACTIVE
),测试可以适应不同的构建模式。bsls_asserttest
组件测试防御性检查的行为,重点在于实现经验、特殊场景(如构造函数测试)以及 C++ 标准中 noexcept
的引入对防御性编程和负向测试的影响。以下是对内容的理解和总结:vector
的内存分配),而负向测试可能依赖这些状态。BSLS_ASSERTTEST_ASSERT_SAFE_FAIL
仅在“安全模式”(Safe Mode,例如 BDE_BUILD_TARGET_SAFE
)下启用。BSLS_ASSERTTEST_ASSERT_SAFE_PASS
则始终运行(无论构建模式)。push_back
)仅在某些模式下运行,负向测试可能面临不一致的状态,导致清理逻辑复杂。bsl::vector
bsl::vector<int> v;
bsls_Assert::setFailureHandler(&bsls_AssertTest::failTestDriver);
BSLS_ASSERTTEST_ASSERT_SAFE_FAIL(v[0]); // 空向量,访问 v[0] 应失败
v.push_back(9); // 添加元素,改变状态
BSLS_ASSERTTEST_ASSERT_SAFE_PASS(v[0]); // 现在 v[0] 合法,期望通过
BSLS_ASSERTTEST_ASSERT_SAFE_FAIL(v[1]); // v[1] 越界,期望失败
v.push_back(42); // 再次改变状态
BSLS_ASSERTTEST_ASSERT_SAFE_PASS(v[0]); // v[0] 合法,期望通过
BSLS_ASSERTTEST_ASSERT_SAFE_PASS(v[1]); // v[1] 合法,期望通过
BSLS_ASSERTTEST_ASSERT_SAFE_FAIL(v[2]); // v[2] 越界,期望失败
v.pop_back(); // 移除元素,改变状态
BSLS_ASSERTTEST_ASSERT_SAFE_PASS(v[0]); // v[0] 仍合法,期望通过
BSLS_ASSERTTEST_ASSERT_SAFE_FAIL(v[1]); // v[1] 越界,期望失败
v.push_back
和 v.pop_back
修改了向量状态,影响后续测试。BSLS_ASSERTTEST_ASSERT_SAFE_PASS
总是执行,确保状态变更(如 push_back
)在所有模式下发生。BSLS_ASSERTTEST_ASSERT_SAFE_FAIL
仅在 SAFE
模式下运行,避免在禁用 BSLS_ASSERT_SAFE
的模式下测试不一致。push_back
)不运行,可能会导致内存泄漏或状态不一致,清理代码需要额外处理不同模式。MyDateImpUtil::toSerial
)抛出测试异常(bsls_AssertTestException
),会导致测试失败。MyDate::MyDate(int y, int m, int d)
: d_serialDate(MyDateImpUtil::toSerial(y, m, d)) // 初始化列表可能触发断言
{
BSLS_ASSERT(MyDate::isValid(y, m, d)); // 太晚了!
}
MyDateImpUtil::toSerial
可能在初始化列表中触发断言,导致 BSLS_ASSERT
尚未执行。toSerial
抛出测试异常,异常信息中的文件名可能不是预期的(例如不是 MyDate
所在的源文件)。_RAW
宏:
BSLS_ASSERTTEST_ASSERT_FAIL_RAW
和 BSLS_ASSERTTEST_ASSERT_PASS_RAW
),仅放宽文件名检查。bsls_Assert::setFailureHandler(&bsls_AssertTest::failTest);
BSLS_ASSERTTEST_ASSERT_PASS_RAW(MyDate(2000, 7, 15)); // 合法日期,期望通过
BSLS_ASSERTTEST_ASSERT_FAIL_RAW(MyDate(0, 7, 15)); // 年份无效,期望失败
BSLS_ASSERTTEST_ASSERT_FAIL_RAW(MyDate(10000, 7, 15)); // 年份无效,期望失败
BSLS_ASSERTTEST_ASSERT_FAIL_RAW(MyDate(2000, 0, 15)); // 月份无效,期望失败
BSLS_ASSERTTEST_ASSERT_FAIL_RAW(MyDate(2000, 13, 15)); // 月份无效,期望失败
BSLS_ASSERTTEST_ASSERT_FAIL_RAW(MyDate(2000, 7, 0)); // 日期无效,期望失败
BSLS_ASSERTTEST_ASSERT_FAIL_RAW(MyDate(2000, 7, 32)); // 日期无效,期望失败
_RAW
宏:放宽文件名检查,允许初始化列表中的断言触发时不验证文件名。MyDate(2000, 7, 15)
应通过。0
或 10000
)、月份(0
或 13
)、日期(0
或 32
)超出范围,期望触发断言。SAFE_2
模式下的检查SAFE_2
模式(BDE_BUILD_TARGET_SAFE_2
)下启用,测试代码需要匹配这种条件。#if defined(BSLS_BUILD_TARGET_SAFE_2)
BSLS_ASSERTTEST_ASSERT_SAFE_FAIL(…);
BSLS_ASSERTTEST_ASSERT_SAFE_PASS(…);
#endif
SAFE_2
模式下测试对应的防御性检查,避免在其他模式下运行不一致的测试。bsls_asserttest
组件,捕获断言触发的异常(bsls_AssertTestException
),从而安全地测试未定义行为。SAFE
、SAFE_2
)启用不同级别的断言,负向测试需要匹配当前模式,确保测试一致性。BSLS_ASSERTTEST_ASSERT_SAFE_FAIL
)依赖构建模式,仅在断言启用时运行。_RAW
宏放宽文件名检查,允许测试继续验证断言行为。SAFE_2
模式下的检查?
#if defined(BSLS_BUILD_TARGET_SAFE_2)
)包裹测试代码,确保仅在对应模式下运行。noexcept
(2011 年引入)noexcept
异常规范。noexcept
确保不抛出异常。noexcept
可带来速度和空间优化(例如编译器可省略异常处理代码)。noexcept
函数尝试抛出异常,程序会直接终止(调用 std::terminate
)。noexcept
,包括窄契约函数(如 vector::front()
和 vector::operator[]
)。vector
的 front()
),防御性检查可能抛出异常(如 bsls_AssertTestException
),但 noexcept
会导致程序终止,无法捕获异常。noexcept
应仅用于移动构造函数和移动赋值(语义需求)。noexcept
可用于宽契约函数,且保证不抛出异常(75% 共识)。noexcept
可用于任何契约中永不抛出的操作(作者强烈反对:“Over my dead body!”)。noexcept
应用于整个 C++ 标准库的每个函数。noexcept
与窄契约函数冲突:防御性检查可能抛出异常(如测试异常),但 noexcept
会终止程序,破坏测试流程。noexcept
,以保留防御性检查的灵活性。bsls_AssertTestException
)来验证断言行为。noexcept
,测试无法捕获异常,导致测试失败或程序终止。.h
、.cpp
和测试驱动文件(.t.cpp
)。bsls_assert
组件):bsls_assert
组件允许应用和库开发者协作,平衡检查时间和处理方式。bsls_asserttest
组件):bsls_asserttest
使负向测试实用、高效。_RAW
宏解决。_RAW
宏放宽文件名检查。SAFE_2
模式检查通过条件编译测试,确保一致性。noexcept
:
noexcept
的引入优化了移动语义和性能,但与窄契约函数冲突,可能影响防御性检查和负向测试。noexcept
使用,避免在窄契约函数上应用。bsls_assert
和 bsls_asserttest
组件提供了强大工具,支持灵活的检查和测试。noexcept
)需要与防御性编程策略协调,以避免冲突。