C++初学者指南-4.诊断---基础:警告和测试

发布于:2024-07-07 ⋅ 阅读:(118) ⋅ 点赞:(0)

C++初学者指南-4.诊断—基础知识:警告和测试

1. 术语和技术

Warnings 警告 编译器消息提示潜在的运行时行为问题的陷阱 (见下文)
Assertions 断言 用于比较和报告表达式的预期值和实际值的语句 (见下文)
Testing 测试 比较程序部分或整体的实际行为和预期行为 (见下文)
Code Coverage 代码覆盖率 实际执行或测试的代码量 gcov…
Static Analysis 静态分析 通过分析源代码,发现潜在的运行时问题,比如未定义的行为 ASAN UBSAN
Dynamic Analysis 动态分析 通过运行实际程序来发现潜在问题,比如内存泄漏 valgrind
Debugging 调试 在运行时单步执行代码并检查内存中值 (接下来)
Profiling 分析 找出每个函数/循环/代码块对总运行时间、内存消耗的影响有多大
Micro Benchmarking 微基准测试 衡量单个函数或一组语句/调用运行时间的小测试,而不是整个程序运行

记住:使用专用类型!

  • 限制输入参数值
  • 确保中间结果的有效性
  • 保证返回值的有效性

目标:编译器作为正确性检查工具-如果编译成功,那就应该是正确的。

// 输入保证:角度以弧度为单位
Square make_rotated (Square const&, Radians angle);
// 输入保证:仅有效数量(例如 > 0)
Gadget duplicate (Gadget const& original,  Quantity times);
//  结果保证:向量已经归一化
UnitVector3d dominant_direction (WindField const&);
// 避免混淆,使用一个好的单位库
si::kg mass (EllipsoidShell const&, si::g_cm3 density);
…

2.编译器警告

编译器错误 = 程序不可编译
编译器警告 = 程序可编译,但有一段有问题的代码可能会导致运行时错误

Gcc/CLang 编译器选项

重要

-Wall 强烈推荐。你应该始终至少使用这个。它并不完全启用所有警告,而是启用那些最重要的,不会产生太多误报的警告。
-Wextra 启用比 -Wall 还多的警告。强烈推荐
-Wpedantic 强烈推荐。需按照严格的ISO C++发出所有警告,拒绝编译器特定的扩展
-Wshadow 强烈建议。当变量或类型声明相互遮蔽时发出警告
-Werror 将所有警告视为错误⇒任何警告都将终止编译
-fsanitize=undefined,address 启用未定义行为检测器和地址检测器(后文将更详细介绍)

推荐的编译选项(生产级别)
-Wall -Wextra -Wpedantic -Wshadow -Wconversion -Werror -fsanitize=undefined,address -Wfloat-equal -Wformat-nonliteral -Wformat-security -Wformat-y2k -Wformat=2 -Wimport -Winvalid-pch -Wlogical-op -Wmissing-declarations -Wmissing-field-initializers -Wmissing-format-attribute -Wmissing-include-dirs -Wmissing-noreturn -Wnested-externs -Wpacked -Wpointer-arith -Wredundant-decls -Wstack-protector -Wstrict-null-sentinel -Wswitch-enum -Wundef -Wwrite-strings

高性能/低内存/安全
注:这里的吵和噪音是指有很多警告的意思
可能会很吵!
-Wdisabled-optimization -Wpadded -Wsign-conversion -Wsign-promo -Wstrict-aliasing=2 -Wstrict-overflow=5 -Wunused -Wunused-parameter

MS Visual Studio 编译器选项

/W1 1 级:严重警告
/W2 2 级:重大警告
/W3 3 级:生产级别警告。 您应该始终至少使用这个。 也是较新的 Visual Studio 项目的默认值。
/W4 强烈推荐,特别是对于新项目。 并没有真正启用所有警告,而是启用最多的警告 重要的不会产生太多误报噪音。
/Wall 启用比 4 级更多的警告;可能有点太吵了
/WX 将所有警告视为错误⇒任何警告都会终止编译

3.断言

运行时断言

#include <cassert>
assert(bool_expression);

如果表达式 产生 false,则中止程序
用例:

  • 在运行时检查预期值/条件
  • 验证先决条件(输入值)
  • 验证不变量(例如,中间状态/结果)
  • 验证后置条件(输出/返回值)

应在发布版本中停用运行时断言 以避免任何性能影响。

#include <cassert>
double sqrt (double x) {
  assert( x >= 0 );}
double r = sqrt(-2.3);
$ g++ … -o runtest test.cpp
$ ./runtest
runtest: test.cpp:3: void sqrt(double): Assertion `x >= 0' failed.
Aborted

逗号必须用括号保护
assert 是一个预处理器宏(稍后会详细介绍) 否则逗号将被解释为宏参数分隔符:

assert( min(1,2) == 1 );  //  ERROR
assert((min(1,2) == 1));  //  OK

断言信息
可以使用自定义宏添加(没有标准方法):

#define assertmsg(expr, msg) assert(((void)msg, expr))
assertmsg(1+2==2, "1 plus 1 must be 2");

(不)激活 – g++/clang
通过定义预处理器宏 NDEBUG 来停用断言, 例如,使用编译器开关:

g++ -DNDEBUG …

(不)激活 – MS Visual Studio
断言已被明确激活

  • 如果预处理宏 _DEBUG 被定义,例如,通过编译器开关 /D_DEBUG
  • 如果提供了编译器开关/MDd

如果在项目设置中或使用编译器开关/DNDEBUG定义了预处理宏NDEBUG,那么断言会被明确地禁用。

静态断言(C++11)

static_assert(bool_constexpr, "message");
static_assert(bool_constexpr);  (C++17)

如果编译时常量表达式产生 false,则中止编译。

using index_t = int;
index_t constexpr DIMS = 1;  // oops
void foo () { 
  static_assert(DIMS > 1, "DIMS must be at least 2");}
index_t bar () {
  static_assert(
    std::numeric_limits<index_t>::is_integer &&
    std::numeric_limits<index_t>::is_signed, 
    "index type must be a signed integer");}
$ g++ … test.cpp
test.cpp: In function 'void foo()':
test.cpp:87:19: error: static assertion failed: DIMS must be at least 2
 87 |  static_assert(DIMS > 1, "DIMS must be at least 2");
    |                ~~^~~

4.测试

指南

使用断言
检查类型无法表达或保证的期望和假设:

  • 仅在运行时可用的期望值
  • 前提条件(输入值)
  • 不变量(例如,中间状态/结果)
  • 后置条件(输出/返回值)

在发布版本中应该关闭运行时断言,以避免任何性能影响。

编写测试
一旦确定了函数或类型的基本用途和接口。

  • 更快的开发:减少耗时的日志记录和调试会话需求。
  • 更容易的性能调优:可以持续检查是否仍然正确。
  • 文档:期望/假设被写在代码中。

使用测试框架
更方便,更少出错:预定义检查、设置设施、测试运行器等。
初学者 / 较小的项目: doctest

  • 非常简洁和自我说明的风格
  • 轻松设置:只需包含一个标题
  • 非常快的编译

大型项目:Catch2

  • 与doctest相同的基本理念(doctest是模仿Catch设计的)。
  • 用不同数值执行相同测试的数值生成器。
  • 使用计时器进行微基准测试,取平均值等。
  • 编译速度较慢,设置起来比doctest稍微复杂一些。

doctest 简单测试案例

// 摘自文档测试教程:
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include "doctest.h"
int factorial (int n) {
  if (n <= 1) return n;
  return factorial(n-1) * n;
}
TEST_CASE("testing factorial") {
    CHECK(factorial(0) == 1);
    CHECK(factorial(1) == 1);
    CHECK(factorial(2) == 2);
    CHECK(factorial(3) == 6);
    CHECK(factorial(10) == 3628800);
}
$ g++ … -o runtest test.cpp
$ ./runtest
test.cpp(7) FAILED!
CHECK( factorial(0) == 1 )
with expansion:
CHECK( 0 == 1 )

测试失败,因为factorial的实现无法正确处理 n = 0 的情况。
运行此示例

doctest 子用例

// 摘自文档测试教程:
TEST_CASE("vectors can be sized and resized") {
    std::vector<int> v(5);
    REQUIRE(v.size() == 5);
    REQUIRE(v.capacity() >= 5);
    SUBCASE("push_back increases the size") {
        v.push_back(1);
        CHECK(v.size() == 6);
        CHECK(v.capacity() >= 6);
    }
    SUBCASE("reserve increases the capacity") {
        v.reserve(6);
        CHECK(v.size() == 5);
        CHECK(v.capacity() >= 6);
    }
}

对于每个子用例,测试用例都是从头开始执行的。
当执行每个子用例时,我们知道vector大小为 5,容量至少为 5。 我们在顶层通过 REQUIRE 强制执行这些要求。

  • 如果 CHECK 失败:测试被标记为失败,但执行会继续进行。
  • 如果 REQUIRE 失败:执行停止。
    运行此示例

不要直接使用 cin/cout/cerr!

直接使用全局 I/O 流使得函数或类型难以测试:

void bad_log (State const& s) { std::cout <<}

在函数中:通过引用传递流

struct State { std::string msg;};

void log (std::ostream& os, State const& s) { os << s.msg; }

TEST_CASE("State Log") {
  State s {"expected"};
  std::ostringstream oss;
  log(oss, s);
  CHECK(oss.str() == "expected");
}

运行上面示例

类范围:存储流指针
但是:请尝试编写与流或任何其他特定I/O方法无关的类型。

class Logger {
  std::ostream* os_;
  int count_;
public:
  explicit
  Logger (std::ostream* os): os_{os}, count_{0} {}bool add (std::string_view msg) {
    if (!os_) return false;
    *os_ << count_ <<": "<< msg << '\n';
    ++count_;
    return true;
  }
};

TEST_CASE("Logging") {
  std::ostringstream oss;
  Logger log {&oss};
  log.add("message");
  CHECK(oss.str() == "0: message\n");
}

运行上面示例

附上原文链接
如果文章对您有用,请随手点个赞,谢谢!^_^

在这里插入图片描述