C++工程实践必备技能

Posted C+G

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++工程实践必备技能相关的知识,希望对你有一定的参考价值。

文章目录

单元测试框架

google test是一个C++中常用且历史悠久的测试框架,其他类似且较新的测试框架有 catch2 或 doetest等,这两个测试框架的优势在于引入简单,是完全 head only 的,但是也正是因为 head only 导致编译速度很慢,当然 doctest 还是挺快的,但 catch2 真的编译太慢了。而 googletest 引入就需要我们自行编译了,当然用cmake的话是可以简化这个过程的,gtest 的使用和引入其实也很简单,由于是直接链接编译好的库,所以编译速度是比较快的(最近测试了下,同样以链接库的方式比 doctest 慢一些… )。我现在更推荐使用 doctest 而不是 gtest 了。

本来是想讲 Google test 的使用的(看我开篇就知道),但是使用了 doctest 后,我现在完全放弃了 Googletest,对我而言有以下几点非常好:

  1. 文件轻量,非常轻量,就几个文件,而且代码量好像就7000行,编译速度奇快,而相对的 Googletest 里包含的东西有点多,比如 gmock,对比起来略显重量级。
  2. CLion 对 doctest 的支持更好,每次我用 Googletest 的时候,CLion都需要重新我当前使用的测试框架,而使用doctest后,则完全没有这方面困扰,反应奇快,这也是轻量带来的好处。
  3. api超级友好,用过就真的回不去。虽然断言宏不是很多,但核心观点是它分解了比较表达式,所以会比其他框架用起来方便太多。
  4. 功能丰富(比如支持对模板进行批量测试),尽管代码轻量,但是功能也毫不含糊,感觉比googletest更好用。

如何引入

正如上述所说,doctest 是head-only的,所以仅仅只需要一个 .h 文件即可,但是我建议不要这样,这样编译速度会慢一些,建议使用编译库再链接的方式,这种方式在cmake里面也很简单,如果不懂cmake,可以看看我这期视频:cmake入门

你只需在cmake项目中添加下列代码:

include(FetchContent)
FetchContent_Declare(
        doctest
        GIT_REPOSITORY https://github.com/doctest/doctest.git
        GIT_TAG v2.4.9
        GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(doctest)

target_link_libraries(target doctest_with_main)

这里的仓库链接由于是GitHub上,如果你不会科学上网的话,建议可以去手动下载GitHub上的代码然后 add_subdirectory() 也是一样的。当然也可以把对应的仓库在gitee上创建一个镜像,那么你就可以直接把上面的 GIT_REPOSITORY 换成你镜像的地址了,比如我拉了一个镜像地址如下:https://gitee.com/acking-you/doctest.git 替换即可。

如何使用

开始前,你可以直接去看官方文档,写的也挺详细:官方文档

首先,我们要清楚,一个测试框架,你需要注意的就只有两点:

  1. 如何组织测试 -> 测试宏
  2. 如何进行测试断言 -> 断言宏

通过下面这个简单的测试进行一个简单的讲解:

//这个宏如果是通过链接的方式引入库的话千万不要加,如果是通过直接的include头文件引入的则需要加入
//#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include "doctest.h"

int factorial(int number)  return number <= 1 ? number : factorial(number - 1) * number; 

TEST_CASE("testing the factorial function") 
    CHECK(factorial(1) == 1);
    CHECK(factorial(2) == 2);
    CHECK(factorial(3) == 6);
    CHECK(factorial(10) == 3628800);

上述代码是在测试斐波那契数列的值。

  1. 通过 TEST_CASE 这个宏来组织一个测试,参数是该测试的名字是一个字符串值,在CLion中会以这个名字来标识这个测试。与 googletest 相比,对应的是 TEST 宏,但不同的是 googletest 需要传两个参数,两个都不是字符串,而且必须符合C++变量命名的字符规则,所以不能以空格或者其他非字母数字的任何符号放在其中,这点其实很不方便。

  2. 通过 CHECK 宏来进行断言判断,如果失败了CLion中会有对应的提示。参数是一个判断表达式,不要小看这个宏,它是默认支持几乎所有内置的类型,并且包括stl容器。对应的 googletest 一般使用 EXPECT_EQ() 传递两个参数来进行比较,默认不支持 const char* 类型,需要使用 EXPECT_STREQ ,而 doctest 则不需要有这方面的考虑,只需要关注这个 CHECK 宏即可,当然它也有对应的 CHECK_XX 宏。

测试相关

经过一个小demo的讲解,那么大家对于测试宏有了一定的了解,下面将继续介绍更多的测试宏。

SUBCASE

这个宏用于在TEST_CASE中继续产生更小的分组,然后你可以安全的捕获到外界的变量来使用。因为每个SUBCASE都是完全独立的重新执行,而不是在同一次执行,比如我将下面的代码块分为1、2、3,那么第一个SUBCASE的顺序将会是 1->2->结束 ,第二个SUBCASE的执行顺序将会是 1->3->结束 。如果最外层的代码在 SUBCASE 后面,那么不会被执行,所有的 SUBCASE 执行情况,我们可以看作是从一个树的根节点到子节点的简单遍历,但每次遍历没有前后文关系(也就是每次遍历都是重新执行的)

TEST_CASE("vectors can be sized and resized") 
    std::vector<int> v(5);
	//1
    REQUIRE(v.size() == 5);
    REQUIRE(v.capacity() >= 5);

    SUBCASE("adding to the vector increases it's size") 
        //2
        v.push_back(1);

        CHECK(v.size() == 6);
        CHECK(v.capacity() >= 6);
    
    SUBCASE("reserving increases just the capacity") 
        //3
        v.reserve(6);

        CHECK(v.size() == 5);
        CHECK(v.capacity() >= 6);
    

例如下面这个例子将会输出:

TEST_CASE("lots of nested subcases") 
    cout << endl << "root" << endl;
    SUBCASE("") 
        cout << "1" << endl;
        SUBCASE("")  cout << "1.1" << endl; 
    
    SUBCASE("")    
        cout << "2" << endl;
        SUBCASE("")  cout << "2.1" << endl; 
        SUBCASE("") 
            cout << "2.2" << endl;
            SUBCASE("") 
                cout << "2.2.1" << endl;
                SUBCASE("")  cout << "2.2.1.1" << endl; 
                SUBCASE("")  cout << "2.2.1.2" << endl; 
            
        
        SUBCASE("")  cout << "2.3" << endl; 
        SUBCASE("")  cout << "2.4" << endl; 
    

TEST_SUITE

test suite表示测试集,顾名思义,就是可以把 test case 分组。

比如可以这样写:

TEST_SUITE("math") 
    TEST_CASE("")  // part of the math test suite
    TEST_CASE("")  // part of the math test suite

也可以分开用 TEST_SUITE_BEGIN 和 TEST_SUITE_END 宏来实现:

TEST_SUITE_BEGIN("utils");

TEST_CASE("")  // part of the utils test suite

TEST_SUITE_END();

TEST_CASE("")  // not part of any test suite

分组后的好处当然是可以直接分组执行了。

TEST_CASE_FIXTURE

这个宏是用来直接测试某个类的方法的,相当于是通过继承的方式创建了一个新的类,所以 protect 修饰的东西都能访问,比如:

class UniqueTestsFixture

 private:
	static int uniqueID;

 protected:
	int conn;

 public:
	UniqueTestsFixture()
		: conn(10)
	
	

 protected:
	static int getID()
	
		return ++uniqueID;
	
;

int UniqueTestsFixture::uniqueID = 0;

TEST_CASE_FIXTURE(UniqueTestsFixture, "test get ID")

	REQUIRE(getID() == conn);

TEST_CASE_TEMPLATE

这个宏是用来测试模板的,如果需要测试的模板功能有共通性,只是类型不一致,那么你可以减少重复劳动,直接用这个宏来帮忙实例化再测试。

比如下列代码测试了 std::any 对于接收字符串类型和整数类型的情况测试:

TEST_CASE_TEMPLATE("test std::any as integer", T, char, short, int, long long int) 
	auto v = T();
	std::any var = T();
	CHECK(std::any_cast<T>(var)==v);


TEST_CASE_TEMPLATE("test std::any as string", T, const char*, std::string_view, std::string) 
	T v = "hello world";
	std::any var = v;
	CHECK(std::any_cast<T>(var)==v);

也可用 TEST_CASE_TEMPLATE_DEFINE 先定义一个模板测试,后面再用 TEST_CASE_TEMPLATE_INVOKE 来决定实例化模板的类型:

TEST_CASE_TEMPLATE_DEFINE("test std::any as integer", T,integer) 
	auto v = T();
	std::any var = T();
	CHECK(std::any_cast<T>(var)==v);


TEST_CASE_TEMPLATE_DEFINE("test std::any as string", T,string) 
	T v = "hello world";
	std::any var = v;
	CHECK(std::any_cast<T>(var)==v);


TEST_CASE_TEMPLATE_INVOKE(integer, char, short, int, long long int);
TEST_CASE_TEMPLATE_INVOKE(string, const char*, std::string_view, std::string);

断言相关

doctest的断言宏是很有规律的,它的设计我之前也提到过,是一种尽量以表达式的方式去简化对api的记忆,你只需要清楚三个断言的等级即可,当然如果想要直接通过对应的类似于 gtestEXPECT_XXX 之类的api来进行断言,实际上也是有的。

断言宏一共有以下三个等级:

  • REQUIRE:这个等级算是最高的,如果断言失败,不仅会标记为测试不通过,而且会强制退出测试(也就是后续的测试将不会再进行)。
  • CHECK:如果断言失败,标记为测试不通过,但不会强制退出(也就是后续的测试还是会执行)。
  • WARN:如果断言失败,不会标记测试不通过,也不会强制退出,但是会给出对应的提示。
常用断言宏

下面为常见的宏使用,使用这些宏比直接使用表达式的编译速度要快一点。

<LEVEL> 表示 REQUIRE、CHECK、WARN 三个等级。

  • <LEVEL>_EQ(left, right) - same as <LEVEL>(left == right)
  • <LEVEL>_NE(left, right) - same as <LEVEL>(left != right)
  • <LEVEL>_GT(left, right) - same as <LEVEL>(left > right)
  • <LEVEL>_LT(left, right) - same as <LEVEL>(left < right)
  • <LEVEL>_GE(left, right) - same as <LEVEL>(left >= right)
  • <LEVEL>_LE(left, right) - same as <LEVEL>(left <= right)
  • <LEVEL>_UNARY(expr) - same as <LEVEL>(expr)
  • <LEVEL>_UNARY_FALSE(expr) - same as <LEVEL>_FALSE(expr)

小提示:在引入头文件之前定义 DOCTEST_CONFIG_SUPER_FAST_ASSERTS 这个宏,也可以提升编译速度。

<LEVEL>_MESSAGE :这个宏用于在错误的适合你可以设置对应的提示信息。

同样,你可以为了方便,先通过 INFO 宏来进行提示消息的预设,然后只要出现测试失败,都会提示这个预设的消息。

CHECK_MESSAGE(2==1,"not valid");

比如上面的代码可以用 INFO 宏,写成下面这样:

INFO("not valid")
CHECK(2==1);
常用工具函数

doctest::Contains() 用于判断字符串是包含这其中的字符。

比如下面这个例子:

REQUIRE("foobar" == doctest::Contains("foo"));

doctest::Approx() 用于更精确的比较浮点数。

比如下面这个例子:

REQUIRE(22.0/7 == doctest::Approx(3.141).epsilon(0.01)); // allow for a 1% error

benchmark框架

关于benchmark,我建议使用 nanobench ,同样也是因为引入简单轻量,使用简单且 head only

官方文档如下:https://nanobench.ankerl.com/tutorial.html#usage

如何引入

其实官方文档已经介绍了如何引入,它也是推荐使用下面的方式进行引入:

cmake_minimum_required(VERSION 3.14)
set(CMAKE_CXX_STANDARD 17)

project(
    CMakeNanobenchExample
    VERSION 1.0
    LANGUAGES CXX)

include(FetchContent)

FetchContent_Declare(
    nanobench
    GIT_REPOSITORY https://github.com/martinus/nanobench.git
    GIT_TAG v4.1.0
    GIT_SHALLOW TRUE)

FetchContent_MakeAvailable(nanobench)

add_executable(MyExample my_example.cpp)
target_link_libraries(MyExample PRIVATE nanobench)

如何使用

使用非常简单,不依赖于宏,而是使用对应的类的成员函数。

比如:

#include <nanobench.h>

#include <atomic>

int main() 
    int y = 0;
    std::atomic<int> x(0);
    ankerl::nanobench::Bench().run("compare_exchange_strong", [&] 
        x.compare_exchange_strong(y, 0);
    );

输出如下:

可以看得出来,上述的输出结果其实可以直接copy到markdown中,会被渲染为表格。

  • ns/op:每个bench内容需要经历的时间(ns为单位)。
  • op/s:每秒可以执行多少次操作。
  • err%:运行多次测试的波动情况(误差)。
  • ins/op:每次操作需要多少条指令。
  • cyc/op:每次操作需要多少次时钟周期。
  • bra/op:每次操作有多少次分支预判。
  • miss%:分支预判的miss率。
  • total:本次消耗的总时间。
  • benchmark:对应的名字。

对于不同的机器上述的指标支持程度略有不同,官方的描述为:

CPU statistics like instructions, cycles, branches, branch misses are only available on Linux, through perf events. On some systems you might need to change permissions through perf_event_paranoid or use ACL.

防止被优化

如下示例:

#include <nanobench.h>
#include <thirdparty/doctest/doctest.h>

TEST_CASE("tutorial_fast_v1") 
    uint64_t x = 1;
    ankerl::nanobench::Bench().run("++x", [&]() 
        ++x;
    );

可能无法输出结果,因为x被优化了,所以可以改为下面这样:

#include <nanobench.h>
#include <doctest/doctest.h>

TEST_CASE("tutorial_fast_v2") 
    uint64_t x = 1;
    ankerl::nanobench::Bench().run("++x", [&]() 
        ankerl::nanobench::doNotOptimizeAway(x += 1);
    );

优化不稳定

有些时候输出结果会提示你测试不稳定,你可以按照提示增加 minEpochIterations

比如:

#include <nanobench.h>
#include <doctest/doctest.h>

#include <random>

TEST_CASE("tutorial_fluctuating_v1") 
    std::random_device dev;
    std::mt19937_64 rng(dev());
    ankerl::nanobench::Bench().run("random fluctuations", [&] 
        // each run, perform a random number of rng calls
        auto iterations = rng() & UINT64_C(0xff);
        for (uint64_t i = 0; i < iterations; ++i) 
            ankerl::nanobench::doNotOptimizeAway(rng());
        
    );

输出如下:

我们按照提示修改代码如下:

#include <nanobench.h>
#include <doctest/doctest.h>

#include <random>

TEST_CASE("tutorial_fluctuating_v2") 
    std::random_device dev;
    std::mt19937_64 rng(dev());
    ankerl::nanobench::Bench().minEpochIterations(5000).run(
        "random fluctuations", [&] 
            // each run, perform a random number of rng calls
            auto iterations = rng() & UINT64_C(0xff);
            for (uint64_t i = 0; i < iterations; ++i) 
                ankerl::nanobench::doNotOptimizeAway(rng());
            
        );

结果果然稳定了。

比较测试结果

有时候我们需要对很多测试结果进行比较,在 nanobench 中,很容易做到,只要共用同一个 Bench 对象即可,在开始的时候调用对应的方法。

比如官方给出了一个对比不同随机数生成器的性能的例子:完整代码:example_random_number_generators.cpp

private:
    static constexpr uint64_t rotl(uint64_t x, unsigned k) noexcept 
        return (x << k) | (x >> (64U - k));
    

    uint64_t stateA;
    uint64_t stateB;
;

namespace 

// Benchmarks how fast we can get 64bit random values from Rng.
template <typename Rng>
void bench(ankerl::nanobench::Bench* bench, char const* name) 
    std::random_device dev;
    Rng rng(dev());

    bench->run(name, [&]() 
        auto r = std::uniform_int_distribution<uint64_t>(rng);
        ankerl::nanobench::doNotOptimizeAway(r);
    );


 // namespace

TEST_CASE("example_random_number_generators") 
    // perform a few warmup calls, and since the runtime is not always stable
    // for each generator, increase the number of epochs to get more accurate
    // numbers.
    ankerl::nanobench::Bench b;
    b.title("Random Number Generators")
        .unit("uint64_t")
        .warmup(100)
        .relative(true);
    b.performanceCounters(true);

    // sets the first one as the baseline
    bench<std::default_random_engine>(&b, "std::default_random_engine");
    bench<std::mt19937>(&b, "std::mt19937");
    bench<std::mt19937_64>(&b, "std::mt19937_64");
    bench<std::ranlux24_base>(&b, "std::ranlux24_base")以上是关于C++工程实践必备技能的主要内容,如果未能解决你的问题,请参考以下文章

C++工程实践必备技能

php后台对接ios,安卓,API接口设计和实践完全攻略,涨薪必备技能

助我拿下大厂offer,架构师必备技能

Linux运维工程师必学必备的8项IT技能

《Java Web开发实战》——Java工程师必备干货教材

Java开发必备技能——Java虚拟机