模仿 gTest 从零实现一个测试框架:使用现代 C++ 改造

开发 测试
本文将介绍如何使用现代 C++ 特性优化代码,这些改进使代码更现代化,性能更好,同时保持了原有的功能完整性。

本文将介绍如何使用现代 C++ 特性优化代码,主要包括以下内容。下面,让我们开始代码优化之旅!

1. 单例模式的魔法改造

让我们先看看单例模式的优化:

// 🎯 使用 inline 关键字让编译器更聪明地内联展开
static inline ETest& GetInstance() {
    // 🔒 static 保证线程安全的懒汉式初始化
    static ETest instance;  
    return instance;        // 🔄 返回唯一实例
}

为什么要这样改进呢? 

  • inline 建议编译器将函数内联展开,减少函数调用开销 
  • static 局部变量保证了线程安全的初始化
  • 返回引用避免了不必要的拷贝

为什么要建议使用 inline? 

  • 减少函数调用开销
  • 避免了函数调用时的栈帧创建和销毁
  • 省去了参数传递和返回值复制的开销

编译器优化:

  • 让编译器有机会进行更多的上下文相关优化
  • 可以直接在调用处展开代码,提升执行效率

特别适合单例模式:

  • GetInstance() 经常被调用
  • 函数体积小,非常适合内联
  • 可以和编译器的其他优化更好地配合

注意事项

  • inline 只是对编译器的建议,不是强制命令
  • 现代编译器已经很智能,会自动决定是否内联
  • 但在关键路径上显式标记 inline 仍然是好习惯

2. 现代化的函数处理方式

来看看如何让函数调用更灵活:

// 🎁 使用 std::function 支持各种可调用对象
using TestFunction = std::function<void()>;

// 🌟 支持移动语义的测试用例添加
void AddTest(std::string name, TestFunction test_func) {
    // ⚡️ 使用 emplace_back 直接构造,避免拷贝
    tests_.emplace_back(std::move(name), std::move(test_func));
}

这样改进的好处是:

  • 可以接受 lambda 表达式啦!
  • 支持任何可调用对象,更加灵活
  • 使用移动语义提升性能

为什么使用值传递而不是引用? 

你可能会问:为什么 name 参数要从之前的引用传递 const std::string& 改为值传递?这其实是现代 C++ 的一个最佳实践!

// 🎯 两种方式的对比
void AddTest(std::string name, ...);              // ✅ 值传递方式
void AddTest(const std::string& name, ...);       // ❓ 引用方式

值传递的优势:

  • 临时对象情况
// 场景1: 传入字符串字面量
AddTest("test_name", ...);  // 值传递:0次拷贝(直接移动)
                            // 引用传递:1次拷贝(在emplace_back时)

// 值传递的过程:
// 1. "test_name" -> 创建临时 std::string
// 2. 通过移动构造传入函数
// 3. 通过移动构造存入 vector
// 总计: 1次构造, 2次移动

// 引用传递的过程:
// 1. "test_name" -> 创建临时 std::string (作为引用参数)
// 2. 在 vector.emplace_back 时复制构造
// 总计: 1次构造, 1次拷贝
  • 具名变量情况
std::string name = "test";
AddTest(name, ...);         // 值传递:1次拷贝
                            // 引用传递:1次拷贝(在emplace_back时)

// 值传递的过程:
// 1. name 被拷贝构造到函数参数
// 2. 函数参数被移动构造到 vector
// 总计: 1次拷贝, 1次移动

// 引用传递的过程:
// 1. name 作为引用传入(无开销)
// 2. 在 vector.emplace_back 时拷贝构造
// 总计: 1次拷贝
  • 移动语义情况
std::string name = "test";
AddTest(std::move(name), ...);  

// 值传递的过程:
// 1. name 被移动构造到函数参数
// 2. 函数参数被移动构造到 vector
// 总计: 2次移动

// 引用传递的过程:
// 1. 移动后的 name 作为引用传入
// 2. 在 vector.emplace_back 时移动构造
// 总计: 1次移动

性能分析总结:

  • 临时对象:值传递略胜(避免了一次拷贝)
  • 具名变量:基本持平(都需要一次拷贝)
  • 移动语义:引用传递略胜(少一次移动)

但考虑到:

(1) 代码可维护性

  • 值传递明确表明参数会被存储
  • 避免悬垂引用风险
  • 性能表现更加统一和可预测

(2) 代码清晰度

  • 值传递的语义更清晰
  • 不需要考虑参数生命周期
  • 减少 std::move 的使用场景

(3) 编译器优化

  • 现代编译器对值传递有很好的优化
  • 可以利用 RVO/NRVO 优化
  • 内联时可能消除额外的开销

因此,在这种"参数最终会被存储"的场景下,推荐使用值传递。这种"按值传递并移动"的模式已成为现代 C++ 的最佳实践。✨

3. 性能小贴士

看看这些贴心的性能优化:

// 🎈 构造函数中预分配内存
ETest() { 
    tests_.reserve(100);  // 💫 避免频繁扩容
}

// 🎭 测试用例的完美转发构造
TestCase(std::string n, TestFunction f) 
    : name(std::move(n)),    // 🏃 移动而不是拷贝
      func(std::move(f)) {}  // 💨 同样移动提升性能

这些优化的效果:

  • 预分配内存减少重新分配的次数
  • 移动语义避免不必要的拷贝
  • 构造函数初始化列表更高效

4. 更智能的断言

来看看更强大的断言实现:

#define ASSERT_EQ(expected, actual)                    \
  do {                                                 \
    const auto& exp = (expected);    // 🎯 避免重复求值  \
    const auto& act = (actual);      // 🎯 使用引用     \
    if (exp != act) {               // ⚠️ 比较结果      \
      // ... 错误处理 ...           // 💡 清晰的错误信息 
    }                                                 \
  } while (0)                       // 🛡️ 安全的宏结构

为什么这样写更好:

  • do-while(0) 让宏更安全
  • 引用避免重复计算
  • 清晰的错误信息更容易调试

每个小改进都在让代码变得更好,这就是现代 C++ 的魔力! 

5. 异常安全性与const正确性

// 🛡️ 使用 noexcept 标记不会抛出异常的函数
void RunAllTests() noexcept {
    TestStats stats;
    // ...
}

// ✨ 使用 const 成员函数表明函数不会修改对象状态
void PrintTestSummary(const TestStats& stats) const {
    // ...
}

为什么要使用这些特性?

(1) noexcept 的优势

  • 提供编译器优化机会
  • 明确函数的异常安全性保证
  • 避免异常展开带来的性能开销
  • 在STL容器操作中可能获得更好的性能

(2) const 成员函数的好处

  • 表明函数不会修改对象状态
  • 允许在const对象上调用
  • 提高代码可读性和可维护性
  • 帮助编译器进行优化

6. 字符串视图优化

// 使用 std::string_view 优化字符串处理
void LogError(std::string_view message) {
    std::cout << message << std::endl;
}

std::string_view 的优势:

  • 零拷贝字符串操作
  • 可以直接接受字符串字面量
  • 比 const std::string& 更轻量
  • 适用于只读字符串场景

使用场景:

// 旧方式
void Log(const std::string& msg);  // 可能导致不必要的字符串构造

// 新方式
void Log(std::string_view msg);    // 更高效,没有额外开销

// 使用示例
Log("直接使用字面量");            // ✅ 完全没有构造开销
std::string str = "test";
Log(str);                         // ✅ 也可以接受 string

这些现代C++特性不仅能提升代码的性能,还能增加代码的安全性和可维护性。在适当的场景下使用这些特性,能让我们的代码更加优雅和高效。

总结

主要优化点包括:

  • 使用 std::function 替代函数指针,提供更好的灵活性,支持 lambda 表达式和其他可调用对象
  • 添加 inline 关键字优化单例实现
  • 使用 std::move 和移动语义优化性能
  • 添加 noexcept 标记提供更好的异常安全性保证
  • 使用 const 成员函数增加代码的可维护性
  • 使用 std::string_view 优化字符串处理
  • 改进断言宏的实现,使用 do {...} while(0) 结构确保宏的安全性
  • 为 vector 预留空间,减少重新分配
  • 使用构造函数初始化列表优化对象构造
  • 使用 emplace_back 替代 push_back 提高性能

这些改进使代码更现代化,性能更好,同时保持了原有的功能完整性。

完整代码

完整代码如下:

#ifndef ETEST_H
#define ETEST_H

#include <chrono>
#include <functional>
#include <iomanip>
#include <iostream>
#include <string>
#include <vector>

class ETest {
public: // 公开接口 🌟
  // 获取单例实例 🎯
  static inline ETest &GetInstance() {
    static ETest instance;
    return instance;
  }

  // 测试注册器 📝
  using TestFunction = std::function<void()>;
  void AddTest(std::string name, TestFunction test_func) {
    tests_.emplace_back(std::move(name), std::move(test_func));
  }

  // 测试执行器 ▶️
  void RunAllTests() noexcept {
    TestStats stats;
    std::cout << "\033[1;36m🚀 Starting tests...\033[0m\n";

    for (const auto &test : tests_) {
      stats.total++;
      auto start = std::chrono::high_resolution_clock::now();

      try {
        std::cout << "📋 Running: " << test.name << std::endl;
        test.func();
        stats.passed++;
        std::cout << "\033[1;32m✓ PASSED\033[0m: " << test.name << std::endl;
      } catch (const std::exception &e) {
        stats.failed++;
        std::cout << "\033[1;31m✗ FAILED\033[0m: " << test.name << "\n";
        std::cout << "  Error: " << e.what() << std::endl;
      }

      auto end = std::chrono::high_resolution_clock::now();
      stats.totalTime += std::chrono::duration<double>(end - start).count();
    }

    // 打印统计结果
    PrintTestSummary(stats);
  }

private: // 内部实现 🔒
  // 私有构造函数 - 防止外部创建实例 🔒
  ETest() { tests_.reserve(100); }

  // 删除拷贝和赋值功能 - 确保唯一性 ⛔
  ETest(const ETest &) = delete;
  ETest &operator=(const ETest &) = delete;

  struct TestCase {
    std::string name;  // 测试名称
    TestFunction func; // 测试函数指针 🎯

    // 使用构造函数初始化列表
    TestCase(std::string n, TestFunction f)
        : name(std::move(n)), func(std::move(f)) {}
  };

  std::vector<TestCase> tests_; // 存储所有测试用例 📦

  // 添加测试结果统计
  struct TestStats {
    int total = 0;
    int passed = 0;
    int failed = 0;
    double totalTime = 0.0;
  };

  void PrintTestSummary(const TestStats &stats) const {
    std::cout << "\n=========================\n";
    std::cout << "📊 Test Summary:\n";
    std::cout << "Total: " << stats.total << " tests\n";
    std::cout << "\033[1;32mPassed: " << stats.passed << "\033[0m\n";
    std::cout << "\033[1;31mFailed: " << stats.failed << "\033[0m\n";
    std::cout << "Time: " << std::fixed << std::setprecision(3)
              << stats.totalTime << "s\n";
    std::cout << "=========================\n";
  }
};

// 改进断言宏,使用 constexpr 和 std::string_view
#define ASSERT(condition)                                                      \
  if (!(condition)) {                                                          \
    const auto message = std::string("Assertion failed: ") + #condition;       \
    std::cout << "\033[1;31m" << message << "\033[0m\n"                        \
              << "File: " << std::string_view(__FILE__) << "\n"                \
              << "Line: " << __LINE__ << std::endl;                            \
    throw std::runtime_error(message);                                         \
  }

// 使用模板改进 ASSERT_EQ
#define ASSERT_EQ(expected, actual)                                            \
  do {                                                                         \
    const auto &exp = (expected);                                              \
    const auto &act = (actual);                                                \
    if (exp != act) {                                                          \
      std::ostringstream oss;                                                  \
      oss << "Expected: " << exp << "\nActual: " << act;                       \
      const auto message = oss.str();                                          \
      std::cout << "\033[1;31mAssertion failed\033[0m\n"                       \
                << message << "\nFile: " << std::string_view(__FILE__)         \
                << "\nLine: " << __LINE__ << std::endl;                        \
      throw std::runtime_error(message);                                       \
    }                                                                          \
  } while (0)

#define TEST(name)                                                             \
  void test_##name();                                                          \
  struct Register##name {                                                      \
    Register##name() { ETest::GetInstance().AddTest(#name, test_##name); }     \
  } register##name##Instance;                                                  \
  void test_##name()

#endif

责任编辑:赵宁宁 来源: everystep
相关推荐

2024-09-25 08:28:45

2020-09-24 11:46:03

Promise

2016-09-14 17:48:44

2019-04-24 15:06:37

Http服务器协议

2021-06-30 07:19:36

网络安全

2021-08-04 05:49:40

数据库数时序数据库技术

2011-08-22 13:57:55

gtest

2021-09-02 10:01:58

Spring 容器AOP

2021-10-27 11:29:32

框架Web开发

2010-02-06 09:46:46

C++单向链表

2020-08-07 10:40:56

Node.jsexpress前端

2014-09-25 09:51:29

Android App个人博客

2011-09-16 10:00:56

C++

2010-02-06 13:42:36

C++单件模式

2022-06-20 09:01:56

Plasmo开源

2021-06-08 07:32:01

框架Mock测试

2016-10-20 16:07:11

C++Modern C++异步

2021-05-28 18:12:51

C++设计

2020-08-17 08:20:16

iOSAOP框架

2010-02-01 13:34:07

C++获得系统时间
点赞
收藏

51CTO技术栈公众号