本文讲述了如何在 C++ 代码中使用单元测试覆盖率工具 lcov,以及 gcov 命令的使用。版本是 lcov 2.0gcov 11.4.0

写在前面:lcov 是我在实习期间初次接触到的工具,当时在配置的时候就遇到了大量中文互联网没有任何记录的问题。绝大部分博客对 lcov 工具的介绍仅停留在安装,并没有对它的使用和报告分析做出更进一步的详解,这也是慕雪撰写本文的原因。希望这篇文章能对需要使用 lcov 工具却又苦于没有引导教程的老哥提供一丝丝帮助。

1. 安装

安装 lcov 的方式比较简单,去 github 上下载官方的安装包就可以了。

bash
1
2
3
4
5
6
7
8
9
10
11
# ubuntu安装依赖项
sudo apt-get install -y wget perl \
libcapture-tiny-perl libdatetime-perl \
libdatetime-format-dateparse-perl
# 下载
wget https://github.com/linux-test-project/lcov/releases/download/v2.0/lcov-2.0.tar.gz
# 解压
tar -zxvf lcov-2.0.tar.gz
cd lcov-2.0
# 安装
sudo make install

安装完毕后查看版本号,成功出现版本号则代表安装成功。

bash
1
2
❯ lcov --version
lcov: LCOV version 2.0-1

更详细的 lcov 安装教程详见本站【Linux】lcov2.0 安装和 perl 修改镜像源一文。另外,本文演示所用的单元测试框架 Gtest 也建议安装一下。需要说明的是,lcov 的报告并不依赖于 Gtest 或任何测试框架,只要函数被调用、代码被运行了,它就可以生成覆盖率报告。

2. 基本命令

2.1. 手工执行

lcov 的基本使用方式如下:

首先我们需要用 g++ 命令编译 gtest 写出来的单元测试代码,使用 -lgtest -lgtest_main -pthread 链接 gtest 库和 pthread 库。选项 -ftest-coverage 可以让 g++ 编译器在代码中插入额外的指令,来确认某部分的代码是否执行了,一般要和 -fprofile-arcs 连用才能产生完整的覆盖率报告。

程序运行后会产生.gcda.gcov.gcno 文件,记录了覆盖率信息,lcov 依赖于这些文件产生最终的 html 覆盖率报告。

bash
1
2
3
g++ -std=c++17 test.cpp -o test \
-lgtest -lgtest_main -pthread \
-fprofile-arcs -ftest-coverage -fprofile-update=atomic

g++ 命令最后的 -fprofile-update=atomic 是 lcov 2.0 中需要新增的一个编译选项,否则运行 lcov 的时候会有告警(具体记不清了,最初的记录里面忘记写这一块的内容了)。

使用如上方式编译了单元测试的代码了之后,就可以执行 lcov 命令来生成报告了

bash
1
2
3
4
5
lcov --capture \
--rc branch_coverage=1 \
--directory . \
--output-file coverage.info \
--ignore-errors mismatch

这个命令最终会生成一个 coverage.info 信息文件。其中 --rc branch_coverage=1 是用于开启分支检测的,不指定这个选项,输出的文件中将不包含分支覆盖率信息,只会有行覆盖率信息。选项 --ignore-errors mismatch 是因为 lcov 2.0 版本出现了一些问题,经常会找不到某些函数的符号表(不知道啥情况,lcov 1.6 没有此告警),会有 mismatch 错误,需要将其忽略。

生成了 coverage.info 文件之后,再使用 genhtml 命令将其转化为最终的 html 报告,输出到 coverage_report 目录中。

bash
1
2
3
genhtml coverage.info \
--rc branch_coverage=1 \
--output-directory coverage_report

一切顺利的话,执行了这些命令,你就可以在当前目录下的 coverage_report 子目录中找到 lcov 的 html 报告了。

2.2. makefile

我们可以把上述命令写入一个 makefile 中,这样可以方便我们执行命令。更新了测试源码之后,使用 make locv 就可以生成最新的覆盖率报告。

makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
test:test.cpp
g++ -std=c++17 test.cpp -o test -lgtest -lgtest_main -pthread -fprofile-arcs -ftest-coverage -fprofile-update=atomic

lcov:test.cpp
g++ -std=c++17 test.cpp -o test -lgtest -lgtest_main -pthread -fprofile-arcs -ftest-coverage -fprofile-update=atomic && \
./test && \
gcov -b -c -o . test.cpp && \
lcov --capture \
--rc branch_coverage=1 \
--directory . \
--output-file coverage_all.info \
--ignore-errors mismatch && \
genhtml coverage.info \
--rc branch_coverage=1 \
--ignore-errors mismatch \
--output-directory coverage_report && \
rm *.info

.PHONY:cl
cl:
sudo rm -rf test *.gcno *.gcda *.gcov out

3. Demo 演示

3.1. 基本 demo

下面是一个最简单的 C++ 代码,以及对应的测试处理,首先在 main.hpp 里面定义了一个最基础的相减函数

cpp
1
2
3
4
5
6
7
8
9
// 相减函数
int Sub(int a, int b)
{
if (a > b)
{
return a - b;
}
return b - a;
}

随后,在 test.cpp 中引用这个头文件并调用 Sub 函数

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <gtest/gtest.h>
#include "main.hpp"

TEST(SubTest, SubTest1)
{
EXPECT_EQ(Sub(3, 2), 1); // 期望 result 等于 1
}

int main(int argc, char **argv)
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

使用上文提到的命令,编译和创建 lcov 报告。在 lcov 命令的最后输出中,会包含如下覆盖率信息

plaintext
1
2
3
4
Overall coverage rate:
lines......: 50.3% (83 of 165 lines)
functions......: 46.3% (44 of 95 functions)
branches......: 42.9% (24 of 56 branches)

因为我用的是 WSL2,可以方便的直接打开报告生成目录,查看 index.html 文件。

image.png

可以看到,报告中列出了所有涉及到的文件,以及这些文件的行覆盖率,分支覆盖率,执行次数。

image.png

但是!这里面有大量 C++ 库函数以及 gtest 库的代码,我们自己的代码反而被掩盖过去了,这肯定不是我们想要的结果。毕竟不是自己写的代码都不需要测试覆盖率。所以我们需要做点操作,屏蔽掉所有库函数的报告。

将上文的 lcov 命令的最终输出文件改成 coverage_all.info

bash
1
2
3
4
5
lcov --capture \
--rc branch_coverage=1 \
--directory . \
--output-file coverage_all.info \
--ignore-errors mismatch

然后在 genhtml 命令之前,执行如下命令。这个命令会处理原本生成的全量数据,把里面我们不想要的东西全都删除掉,再生成一个 coverage.info 文件。

bash
1
2
3
4
5
6
7
lcov --remove coverage_all.info \
'*/usr/include/*' '*/usr/lib/*' '*/usr/lib64/*' \
'*/usr/local/include/*' '*/usr/local/lib/*' '*/usr/local/lib64/*' \
--rc branch_coverage=1 \
--output-file coverage.info \
--ignore-errors unused \
--ignore-errors mismatch

随后再执行 genhtml 命令,这一次生成的报告文件就只有我们自己的代码了。

bash
1
2
3
genhtml coverage.info \
--rc branch_coverage=1 \
--output-directory coverage_report

image.png

进入报告中看,其实这里还是有一个需要排除的项目的,即 test.cpp 是单元测试的文件,我们也不需要关注单元测试这个文件本身的覆盖率正常不,当前我们只需要关注 main.hpp 这个功能源码文件的覆盖率。

可以将 test.cpp 也写入上文的 --remove 选项之后,这样它也会被过滤掉。实际项目中,直接过滤单元测试代码文件的目录即可。

另外,使用这个命令,lcov 会在输出中报告没有被匹配上的地址,可以用 --ignore-errors unused 来屏蔽这个告警。

plaintext
1
2
3
4
5
6
7
lcov: WARNING: ('unused') 'exclude' pattern '*/usr/lib/*' is unused.
lcov: WARNING: ('unused') 'exclude' pattern '*/usr/lib64/*' is unused.
(use "lcov --ignore-errors unused,unused ..." to suppress this warning)
lcov: WARNING: ('unused') 'exclude' pattern '*/usr/local/lib/*' is unused.
(use "lcov --ignore-errors unused,unused ..." to suppress this warning)
lcov: WARNING: ('unused') 'exclude' pattern '*/usr/local/lib64/*' is unused.
(use "lcov --ignore-errors unused,unused ..." to suppress this warning)

3.2. 报告基本分析

image.png

点开 main.hpp 文件,可以看到如下报告。其中右上角是当前文件的覆盖率信息,然后会展示文件的源码:

  • 源码每一行之前的数字是这一行被运行了几次;
  • 底色为橙色标注的,就是没有被覆盖的行;
  • 蓝色标注的,则是被覆盖了的行;

在每一个 if 语句的分支点,也会产生一个分支覆盖率报告,这里显示的 [+,-] 代表 if 条件为 true 的分支被命中了,为 false 的分支没有命中。在测试代码中我使用的是 Sub(3, 2) 来调用该函数,参数 a 是大于 b 的(命中 true 分支),也和这里的分支覆盖报告相符。

image.png

这样我们就可以知道,当前需要怎么补充测试用例了。我们需要补充一个 b 比 a 大或者相等的测试用例,追加如下测试调用。

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <gtest/gtest.h>
#include "main.hpp"

TEST(SubTest, SubTest1)
{
EXPECT_EQ(Sub(3, 2), 1); // 期望 result 等于 1
EXPECT_EQ(Sub(2, 4), 2); // 期望 result 等于 2
EXPECT_EQ(Sub(3, 3), 0); // 期望 result 等于 0
}

int main(int argc, char **argv)
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

再次编译执行,重新查看报告。此时可以看到,我们的 main.hpp 已经实现了 100% 的覆盖。

image.png

删除其他测试,留一个 b 比 a 大的测试用例。此时可以看到,分支覆盖显示为 [-,+],代表我们当前分支的 false 条件被命中了,但是 true 条件没有。

image.png

简单总结,单个分支覆盖率中 [] 的逗号左侧是 true,右侧是 false;+ 代表覆盖,- 代表没有覆盖,#代表这个分支没有被执行。

3.3. 多条件判断

上面的 if 语句中我们只写了一个判断条件,实际场景中判断条件不止一个的情况还是经常出现的,给 Sub 函数新增一个参数,再来进行测试。

cpp
1
2
3
4
5
6
7
8
9
10
#include <cstdbool>
// 相减函数,默认是A-B,第三个参数为是否要返回绝对值
int Sub(int a, int b, bool isAbs)
{
if (b < a && isAbs)
{
return b - a;
}
return a - b;
}

测试用例如下

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <gtest/gtest.h>
#include "main.hpp"

TEST(SubTest, SubTest1)
{
// 传入false代表我们想a-b,不需要绝对值
EXPECT_EQ(Sub(2, 4, false), -2); // 期望 result 等于 -2
}

int main(int argc, char **argv)
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

这里可以看到,在 if 语句左侧的分支覆盖率条件的 [] 里面多了一对加减,这里多的就是 isAbs 这个判断条件,分支覆盖率中的每一个判断条件都有一个 true/false 分支,两两一对,从左到右的顺序和我们的判断条件中的条件顺序是一致的。

在上面的测试用例中,我们传入了 b 大于 a 的值,同时 isAbs 是 false,命中了 b > a 为 true 的分支,和 isAbs 为 false 的分支。

image.png

新增一个测试用例,这一次命中的是 isAbs 为 true 的分支。

cpp
1
EXPECT_EQ(Sub(2, 4, true), 2);

报告中,isAbs 为 true 的分支也变成了 + 代表已命中,符合预期。

image.png

再添加一个 a 比 b 大的测试用例,即可将该函数的所有分支覆盖完毕。

plaintext
1
EXPECT_EQ(Sub(6, 4, true), 2);

image.png

4. 引入 gcov 命令

4.1. 基本使用

接下来给大家引入 gcov 命令的使用,gcov 命令可以生成更加详细的关于某个分支为什么没有被覆盖的说明。比如未覆盖的异常分支在生成的源文件.gcov 文件中就会显示出来。

gcov 命令和 gcc/g++ 是同源的,只要你的系统上安装了 gcc,那就会有 gcov 命令。二者的版本号输出都是一致的。

plaintext
1
2
3
4
5
6
7
8
9
10
❯ gcov --version          
gcov (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
❯ gcc --version
gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

执行过 lcov 命令后,在构建目录下会产生很多的.gcov 文件,和我们自己的代码的.gcda 文件

bash
1
2
3
4
5
6
ls
alloc_traits.h.gcov gtest-assertion-result.h.gcov main.hpp stl_iterator_base_funcs.h.gcov test.gcda
basic_string.h.gcov gtest.h.gcov main.hpp.gcov stl_iterator_base_types.h.gcov test.gcno
basic_string.tcc.gcov gtest-internal.h.gcov makefile test tuple.gcov
char_traits.h.gcov gtest-port.h.gcov move.h.gcov test.cpp type_traits.h.gcov
coverage_report gtest-printers.h.gcov new_allocator.h.gcov test.cpp.gcov unique_ptr.h.gcov

我们可以使用 gcov 命令,把这些文件文件转换成可读报告。

bash
1
gcov -b -c -o .gcda文件所在路径 cpp源文件路径

比如这里我们要处理的是 test.gcda,命令如下。

bash
1
gcov -b -c -o . test.cpp

这个命令会生成一个源文件名.gcov 文件,文件中就会有详细的文字说明了。比如 main.hpp.gcov 文件中的描述如下,有每一个分支被命中的次数,后面有个括号是这个分支的说明。

文件中第一列的 -: 代表这一行不统计命中次数,第一列数字: 代表这一行被执行的次数(注意要和第二列的代码行号区分开)。

plaintext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
        -:    0:Source:main.hpp
-: 0:Graph:./test.gcno
-: 0:Data:./test.gcda
-: 0:Runs:1
-: 1:#include <cstdbool>
-: 2:// 相减函数,默认是A-B,第三个参数为是否要返回绝对值
function _Z3Subiib called 3 returned 100% blocks executed 100%
3: 3:int Sub(int a, int b, bool isAbs)
-: 4:{
3: 5: if (b > a && isAbs)
branch 0 taken 2 (fallthrough)
branch 1 taken 1
branch 2 taken 1 (fallthrough)
branch 3 taken 1
-: 6: {
1: 7: return b - a;
-: 8: }
2: 9: return a - b;
-: 10:}

文件中的 (fallthrough) 代表当前 if 分支被跳过。比如下面的代码中,fallthrough 的意思就是当 a 不等于 b 的时候,分支 A 会被跳过,走到分支 B 中。

cpp
1
2
3
4
5
if (a == b){
// A
} else {
// B
}

在上面的例子中,当 b 小于等于 a 或者 isAbs 为假的时候,return b - a; 就会被跳过,落到 return a - b; 分支中。

这里将 isAbs 改成一个函数调用,函数本身参数不变

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <cstdbool>
#include <exception>

bool isAbsFunc(int a, int b, bool isAbs)
{
// 当a为100的时候抛出异常
if (a == 100)
{
throw std::invalid_argument("a should not be 100.");
}
return isAbs;
}

// 相减函数,默认是A-B,第三个参数为是否要返回绝对值
int Sub(int a, int b, bool isAbs)
{
if (b > a && isAbsFunc(a, b, isAbs))
{
return b - a;
}
return a - b;
}

此时分支覆盖率报告如下。因为我们当前的 a 并不等于 100,所以一直命中的都是 a == 100 为 false 的分支,符合预期。但是这里会有一个额外的分支未覆盖情况,即 throw 这一行也出现了一个分支,且 [] 里面的两个符号都是#代表这一行没有被运行

image.png

我们可以用 gcov 命令来看看 throw 这一行的分支覆盖情况。可以看到这里提示 never executed,没有运行。

plaintext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
        -:    0:Source:main.hpp
-: 0:Graph:./test.gcno
-: 0:Data:./test.gcda
-: 0:Runs:1
-: 1:#include <cstdbool>
-: 2:#include <exception>
-: 3:
function _Z9isAbsFunciib called 2 returned 100% blocks executed 50%
2: 4:bool isAbsFunc(int a, int b, bool isAbs)
-: 5:{
-: 6: // 当a为100的时候抛出异常
2: 7: if (a == 100)
branch 0 taken 0 (fallthrough)
branch 1 taken 2
-: 8: {
#####: 9: throw std::invalid_argument("a should not be 100.");
call 0 never executed
call 1 never executed
branch 2 never executed
branch 3 never executed
call 4 never executed
call 5 never executed
-: 10: }
2: 11: return isAbs;
-: 12:}
-: 13:
-: 14:// 相减函数,默认是A-B,第三个参数为是否要返回绝对值
function _Z3Subiib called 3 returned 100% blocks executed 100%
3: 15:int Sub(int a, int b, bool isAbs)
-: 16:{
3: 17: if (b > a && isAbsFunc(a, b, isAbs))
branch 0 taken 2 (fallthrough)
branch 1 taken 1
call 2 returned 2
branch 3 taken 1 (fallthrough)
branch 4 taken 1
branch 5 taken 1 (fallthrough)
branch 6 taken 2
-: 18: {
1: 19: return b - a;
-: 20: }
2: 21: return a - b;
-: 22:}

那我们加一个 a 等于 100 的测试用例呢?

cpp
1
2
// 期望抛出异常
EXPECT_ANY_THROW(Sub(100, 400, true));

此时 gcov 文件会是如下模样,我们 a == 100 的两个分支都命中了,但是你会发现,它有一个 branch 3 taken 0 (throw) 为 0 次命中,没有被覆盖上。

plaintext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function _Z9isAbsFunciib called 3 returned 67% blocks executed 88%
3: 4:bool isAbsFunc(int a, int b, bool isAbs)
-: 5:{
-: 6: // 当a为100的时候抛出异常
3: 7: if (a == 100)
branch 0 taken 1 (fallthrough)
branch 1 taken 2
-: 8: {
1: 9: throw std::invalid_argument("a should not be 100.");
call 0 returned 1
call 1 returned 1
branch 2 taken 1 (fallthrough)
branch 3 taken 0 (throw)
call 4 returned 0
call 5 never executed
-: 10: }
2: 11: return isAbs;
-: 12:}

在 lcov 报告中也是如此,会显示有一个没有覆盖的 (throw) 抛异常分支。

image.png

4.2. lcov 过滤 std 库函数造成的分支

这就涉及到 lcov 的一个不那么容易找到的设置了,当时百度了老久,最后还是去 Github 翻 issue 才得到的答案。下面贴出几个相关的 issue

简而言之,lcov 支持过滤掉这类由 std 库造成的无法覆盖的异常分支。只需要在 lcov 命令和 genhtml 命令中加上 --filter branch 选项即可。添加了这个命令后,可以看到 throw 这一行的分支被过滤不显示了。

image.png

即便我们没有命中 a == 100 的情况,throw 这一行也不会出现 [##] 的未命中分支。

image.png

再举个 map 的 emplace 的例子,代码如下

cpp
1
2
3
4
5
void EmplaceMap(int key, int value)
{
static std::map<int, std::set<int>> mapValue;
mapValue[key].emplace(value);
}

测试代码

cpp
1
2
3
4
5
TEST(EmplaceMapTest,EmplaceMapTest1)
{
EXPECT_NO_THROW(EmplaceMap(1,2));
EXPECT_NO_THROW(EmplaceMap(1,3));
}

可以看到,如果不加上 --filter branch 过滤选项,在 lcov 报告中,即便这一行是完全不存在任何分支的,也会出现一个未覆盖的情况。

image.png

生成的 gcov 文件如下,这里会有一个不知道什么由来的 branch 4 没有被覆盖到。

plaintext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function _Z10EmplaceMapii called 2 returned 100% blocks executed 100%
2: 6:void EmplaceMap(int key, int value)
-: 7:{
2: 8: static std::map<int, std::set<int>> mapValue;
branch 0 taken 1 (fallthrough)
branch 1 taken 1
call 2 returned 1
branch 3 taken 1 (fallthrough)
branch 4 taken 0
call 5 returned 1
call 6 returned 1
call 7 returned 1
2: 9: mapValue[key].emplace(value);
call 0 returned 2
call 1 returned 2
2: 10:}
-: 11:

加上了 --filter branch 过滤选项之后,这一行则完全不会有分支覆盖率信息。这才是我们预期的输出,因为我们不应该关注不是我们自己写的代码(比如 std 库和第三方库)中的分支。

image.png

另外,过滤选项默认只对常见的 cpp 头文件起效。对诸如.inl 这种头文件是不起效果的。可以使用如下命令,修改默认的 c_file_extensions 后缀名配置,添加你需要的文件后缀。

bash
1
--rc c_file_extensions=c,cpp,hpp,h,inl

关联 issue:https://github.com/linux-test-project/lcov/issues/250

5. 一些 lcov 报告问题的记录

经过上面的步骤,想必你已经知道怎么去使用 lcov 了。下面是我在使用 lcov 过程中遇到的一些报告的共性问题,记录于此,经供参考。

5.1. lcov 屏蔽语法

lcov 本身也支持通过在代码中添加注释的方式来屏蔽一些代码的覆盖率检测。屏蔽的语法分为单行代码和多行屏蔽。

cpp
1
2
3
4
5
// LCOV_EXCL_BR_START
多行代码
// LCOV_EXCL_BR_STOP

单行代码 // LCOV_EXCL_BR_LINE

在实际代码中,可能会有一些 linux 库函数调用这类难以复现失败场景的函数调用,又没有办法被过滤掉的分支。这种情况就可以在注明原因以后,使用 lcov 的屏蔽注释将其屏蔽掉,让最终生成的报告里面没有这些难以覆盖的错误情况。

5.2. assert 假分支无法覆盖

如下图所示,lcov 的 assert 始终只会覆盖假的分支,因为分支为真的时候就直接程序终止了。

image.png

Gtest 中有一个 EXPECT_DEATH 可以用来测试 assert 为真的情况,但即便使用了这个宏,lcov 和 gcov 依旧无法生成命中的报告。所以,推荐的做法是在编译单元测试代码的时候使用 -DNDEBUG 宏直接禁用所有 assert,这样就不会有关于 assert 的分支覆盖率报告了。

5.3. trylock 分支覆盖

一般情况下,在我们的测试场景中不太好复现 try_lock() 函数调用失败的分支,这需要有一个多线程的场景,但多线程操作共享资源的运行顺序本身就是不可预知的,不太好在单元测试中构建出一个一定冲突的场景来。

image.png

这时候可以用一种黑魔法,在单元测试中,取出类的私有成员变量 mutex,将其 lock 了之后,再去调用包含 try_lock() 调用和判断的函数。函数调用完毕后,再 unlock 解锁。

如下是这个黑魔法的源码和使用示例,注意只有 g++ 使用 -std=c++17 之后才支持编译这个代码。在 windows 的 vs2019 下这个特性是编译不过的,即便设置了 C++17 也不行,可能是我的配置不对。

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#pragma once
// C++17才支持,通过友元和元组,取出任意成员变量
template <class T, auto... Member>
struct StealMember
{
friend auto StealClass(T &t)
{
return std::make_tuple(Member...);
}
};

// 使用示例:
// // 1.友元函数声明,TestClass是我们需要操作的目标类。
auto StealClass(TestClass &t);
// // 2.在下面的模板中添加需要的私有函数或成员。
template class StealMember<TestClass,
&TestClass::GetA, &TestClass::_a, &TestClass::_b>;
// // 3.在需要的函数中使用如下方式取成员变量。
void TestFunc() {
TestClass t1(20, 300.23);
auto tp = StealClass(t1); // 构建元组
cout << "GetA: " << (t1.*(std::get<0>(tp)))() << endl;
cout << "_a: " << (t1.*(std::get<1>(tp))) << endl;
}

这个代码首先声明了友元函数,StealClass 会被声明成 TestClass 的友元函数,从而可以读取到该类的私有成员。随后在实例化 StealMember 模板的时候,指定了目标类和其私有成员,这样 StealClass 函数就可以在调用的时候,给我们返回一个包含私有成员指针的元组

重点来了:C++ 在实例化模板的时候,不会去检查成员的访问限定符。

有了元组,就可以用 std::get<元组内元素下标>(元组对象) 的方式取出元组的某一个成员,即私有成员的指针。有了私有成员的指针之后,我们就可以使用对象.(*私有成员指针) 的方式访问到一个私有成员变量或者成员函数了。

关于这个特性的更多介绍,可以参考下面的资料

咋样,是不是很 “黑魔法” 呢?

5.4. string 相加的时候会有大量无法覆盖的异常分支

如下图所示,这个函数中调用了 string 的相加操作,造成了大量的没有覆盖的异常分支。

image.png

在 gcov 报告中可以更详细的看到没有被覆盖的分支都是什么,大多都是和 throw 有关的。

plaintext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
function _Z16test_string_plusRKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEES6_ called 2 returned 100% blocks executed 69%
2: 48:void test_string_plus(const string& local ,const string& remote)
-: 49:{
2: 50: static string recv_msg;
branch 0 taken 1 (fallthrough)
branch 1 taken 1
call 2 returned 1
branch 3 taken 1 (fallthrough)
branch 4 taken 0
call 5 returned 1
call 6 returned 1
call 7 returned 1
2: 51: static_cast<void>(recv_msg.assign("/" + (local < remote ? local + "_" + remote : remote + "_" + local)));
call 0 returned 2
branch 1 taken 1 (fallthrough)
branch 2 taken 1
call 3 returned 1
branch 4 taken 1 (fallthrough)
branch 5 taken 0 (throw)
call 6 returned 1
branch 7 taken 1 (fallthrough)
branch 8 taken 0 (throw)
call 9 returned 1
branch 10 taken 1 (fallthrough)
branch 11 taken 0 (throw)
call 12 returned 1
branch 13 taken 1 (fallthrough)
branch 14 taken 0 (throw)
call 15 returned 2
branch 16 taken 2 (fallthrough)
branch 17 taken 0 (throw)
call 18 returned 2
call 19 returned 2
call 20 returned 2
branch 21 taken 1 (fallthrough)
branch 22 taken 1
call 23 returned 1
branch 24 taken 1 (fallthrough)
branch 25 taken 1
call 26 returned 1
call 27 never executed
branch 28 never executed
branch 29 never executed
call 30 never executed
branch 31 never executed
branch 32 never executed
call 33 never executed
2: 52:}

可当前我已经添加了过滤命令了,为什么没有生效呢?

实际上,将上面的代码改成下面的 if/else 逻辑,就不会有这么多的异常分支了。

image.png

这是我的猜想:lcov 的过滤命令在检测到某一行中有用户定义的判断条件 local < remote 的时候就会失效,因为可能会错误过滤掉用户自己的分支。与其错报一万不可少报一个,于是就把所有的异常分支都展现出来了。

三目运算符改成 if/else 了之后,用户定义的判断条件和 string 的相加操作隔离开了,就能正常进行过滤了。所以,在编写优化分支覆盖率的代码的时候,可以考虑将

6. The end

其实在最开始的时候我记录了更多 lcov 相关的错误,但大部分错误都可以使用 --filter branch 选项过滤掉,且有一部分错误在我当前的环境中并没有被复现出来,故此不记录于本文中。

如果你遇到了本文没有记录的问题,欢迎在评论区留言交流。