模仿 gTest 從零實現(xiàn)一個測試框架:使用現(xiàn)代 C++ 改造
本文將介紹如何使用現(xiàn)代 C++ 特性優(yōu)化代碼,主要包括以下內(nèi)容。下面,讓我們開始代碼優(yōu)化之旅!
1. 單例模式的魔法改造
讓我們先看看單例模式的優(yōu)化:
// ?? 使用 inline 關鍵字讓編譯器更聰明地內(nèi)聯(lián)展開
static inline ETest& GetInstance() {
// ?? static 保證線程安全的懶漢式初始化
static ETest instance;
return instance; // ?? 返回唯一實例
}
為什么要這樣改進呢?
- inline 建議編譯器將函數(shù)內(nèi)聯(lián)展開,減少函數(shù)調(diào)用開銷
- static 局部變量保證了線程安全的初始化
- 返回引用避免了不必要的拷貝
為什么要建議使用 inline?
- 減少函數(shù)調(diào)用開銷
- 避免了函數(shù)調(diào)用時的棧幀創(chuàng)建和銷毀
- 省去了參數(shù)傳遞和返回值復制的開銷
編譯器優(yōu)化:
- 讓編譯器有機會進行更多的上下文相關優(yōu)化
- 可以直接在調(diào)用處展開代碼,提升執(zhí)行效率
特別適合單例模式:
- GetInstance() 經(jīng)常被調(diào)用
- 函數(shù)體積小,非常適合內(nèi)聯(lián)
- 可以和編譯器的其他優(yōu)化更好地配合
注意事項
- inline 只是對編譯器的建議,不是強制命令
- 現(xiàn)代編譯器已經(jīng)很智能,會自動決定是否內(nèi)聯(lián)
- 但在關鍵路徑上顯式標記 inline 仍然是好習慣
2. 現(xiàn)代化的函數(shù)處理方式
來看看如何讓函數(shù)調(diào)用更靈活:
// ?? 使用 std::function 支持各種可調(diào)用對象
using TestFunction = std::function<void()>;
// ?? 支持移動語義的測試用例添加
void AddTest(std::string name, TestFunction test_func) {
// ?? 使用 emplace_back 直接構(gòu)造,避免拷貝
tests_.emplace_back(std::move(name), std::move(test_func));
}
這樣改進的好處是:
- 可以接受 lambda 表達式啦!
- 支持任何可調(diào)用對象,更加靈活
- 使用移動語義提升性能
為什么使用值傳遞而不是引用?
你可能會問:為什么 name 參數(shù)要從之前的引用傳遞 const std::string& 改為值傳遞?這其實是現(xiàn)代 C++ 的一個最佳實踐!
// ?? 兩種方式的對比
void AddTest(std::string name, ...); // ? 值傳遞方式
void AddTest(const std::string& name, ...); // ? 引用方式
值傳遞的優(yōu)勢:
- 臨時對象情況
// 場景1: 傳入字符串字面量
AddTest("test_name", ...); // 值傳遞:0次拷貝(直接移動)
// 引用傳遞:1次拷貝(在emplace_back時)
// 值傳遞的過程:
// 1. "test_name" -> 創(chuàng)建臨時 std::string
// 2. 通過移動構(gòu)造傳入函數(shù)
// 3. 通過移動構(gòu)造存入 vector
// 總計: 1次構(gòu)造, 2次移動
// 引用傳遞的過程:
// 1. "test_name" -> 創(chuàng)建臨時 std::string (作為引用參數(shù))
// 2. 在 vector.emplace_back 時復制構(gòu)造
// 總計: 1次構(gòu)造, 1次拷貝
- 具名變量情況
std::string name = "test";
AddTest(name, ...); // 值傳遞:1次拷貝
// 引用傳遞:1次拷貝(在emplace_back時)
// 值傳遞的過程:
// 1. name 被拷貝構(gòu)造到函數(shù)參數(shù)
// 2. 函數(shù)參數(shù)被移動構(gòu)造到 vector
// 總計: 1次拷貝, 1次移動
// 引用傳遞的過程:
// 1. name 作為引用傳入(無開銷)
// 2. 在 vector.emplace_back 時拷貝構(gòu)造
// 總計: 1次拷貝
- 移動語義情況
std::string name = "test";
AddTest(std::move(name), ...);
// 值傳遞的過程:
// 1. name 被移動構(gòu)造到函數(shù)參數(shù)
// 2. 函數(shù)參數(shù)被移動構(gòu)造到 vector
// 總計: 2次移動
// 引用傳遞的過程:
// 1. 移動后的 name 作為引用傳入
// 2. 在 vector.emplace_back 時移動構(gòu)造
// 總計: 1次移動
性能分析總結(jié):
- 臨時對象:值傳遞略勝(避免了一次拷貝)
- 具名變量:基本持平(都需要一次拷貝)
- 移動語義:引用傳遞略勝(少一次移動)
但考慮到:
(1) 代碼可維護性
- 值傳遞明確表明參數(shù)會被存儲
- 避免懸垂引用風險
- 性能表現(xiàn)更加統(tǒng)一和可預測
(2) 代碼清晰度
- 值傳遞的語義更清晰
- 不需要考慮參數(shù)生命周期
- 減少 std::move 的使用場景
(3) 編譯器優(yōu)化
- 現(xiàn)代編譯器對值傳遞有很好的優(yōu)化
- 可以利用 RVO/NRVO 優(yōu)化
- 內(nèi)聯(lián)時可能消除額外的開銷
因此,在這種"參數(shù)最終會被存儲"的場景下,推薦使用值傳遞。這種"按值傳遞并移動"的模式已成為現(xiàn)代 C++ 的最佳實踐。?
3. 性能小貼士
看看這些貼心的性能優(yōu)化:
// ?? 構(gòu)造函數(shù)中預分配內(nèi)存
ETest() {
tests_.reserve(100); // ?? 避免頻繁擴容
}
// ?? 測試用例的完美轉(zhuǎn)發(fā)構(gòu)造
TestCase(std::string n, TestFunction f)
: name(std::move(n)), // ?? 移動而不是拷貝
func(std::move(f)) {} // ?? 同樣移動提升性能
這些優(yōu)化的效果:
- 預分配內(nèi)存減少重新分配的次數(shù)
- 移動語義避免不必要的拷貝
- 構(gòu)造函數(shù)初始化列表更高效
4. 更智能的斷言
來看看更強大的斷言實現(xiàn):
#define ASSERT_EQ(expected, actual) \
do { \
const auto& exp = (expected); // ?? 避免重復求值 \
const auto& act = (actual); // ?? 使用引用 \
if (exp != act) { // ?? 比較結(jié)果 \
// ... 錯誤處理 ... // ?? 清晰的錯誤信息
} \
} while (0) // ??? 安全的宏結(jié)構(gòu)
為什么這樣寫更好:
- do-while(0) 讓宏更安全
- 引用避免重復計算
- 清晰的錯誤信息更容易調(diào)試
每個小改進都在讓代碼變得更好,這就是現(xiàn)代 C++ 的魔力!
5. 異常安全性與const正確性
// ??? 使用 noexcept 標記不會拋出異常的函數(shù)
void RunAllTests() noexcept {
TestStats stats;
// ...
}
// ? 使用 const 成員函數(shù)表明函數(shù)不會修改對象狀態(tài)
void PrintTestSummary(const TestStats& stats) const {
// ...
}
為什么要使用這些特性?
(1) noexcept 的優(yōu)勢
- 提供編譯器優(yōu)化機會
- 明確函數(shù)的異常安全性保證
- 避免異常展開帶來的性能開銷
- 在STL容器操作中可能獲得更好的性能
(2) const 成員函數(shù)的好處
- 表明函數(shù)不會修改對象狀態(tài)
- 允許在const對象上調(diào)用
- 提高代碼可讀性和可維護性
- 幫助編譯器進行優(yōu)化
6. 字符串視圖優(yōu)化
// 使用 std::string_view 優(yōu)化字符串處理
void LogError(std::string_view message) {
std::cout << message << std::endl;
}
std::string_view 的優(yōu)勢:
- 零拷貝字符串操作
- 可以直接接受字符串字面量
- 比 const std::string& 更輕量
- 適用于只讀字符串場景
使用場景:
// 舊方式
void Log(const std::string& msg); // 可能導致不必要的字符串構(gòu)造
// 新方式
void Log(std::string_view msg); // 更高效,沒有額外開銷
// 使用示例
Log("直接使用字面量"); // ? 完全沒有構(gòu)造開銷
std::string str = "test";
Log(str); // ? 也可以接受 string
這些現(xiàn)代C++特性不僅能提升代碼的性能,還能增加代碼的安全性和可維護性。在適當?shù)膱鼍跋率褂眠@些特性,能讓我們的代碼更加優(yōu)雅和高效。
總結(jié)
主要優(yōu)化點包括:
- 使用 std::function 替代函數(shù)指針,提供更好的靈活性,支持 lambda 表達式和其他可調(diào)用對象
- 添加 inline 關鍵字優(yōu)化單例實現(xiàn)
- 使用 std::move 和移動語義優(yōu)化性能
- 添加 noexcept 標記提供更好的異常安全性保證
- 使用 const 成員函數(shù)增加代碼的可維護性
- 使用 std::string_view 優(yōu)化字符串處理
- 改進斷言宏的實現(xiàn),使用 do {...} while(0) 結(jié)構(gòu)確保宏的安全性
- 為 vector 預留空間,減少重新分配
- 使用構(gòu)造函數(shù)初始化列表優(yōu)化對象構(gòu)造
- 使用 emplace_back 替代 push_back 提高性能
這些改進使代碼更現(xiàn)代化,性能更好,同時保持了原有的功能完整性。
完整代碼
完整代碼如下:
#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));
}
// 測試執(zhí)行器 ??
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();
}
// 打印統(tǒng)計結(jié)果
PrintTestSummary(stats);
}
private: // 內(nèi)部實現(xiàn) ??
// 私有構(gòu)造函數(shù) - 防止外部創(chuàng)建實例 ??
ETest() { tests_.reserve(100); }
// 刪除拷貝和賦值功能 - 確保唯一性 ?
ETest(const ETest &) = delete;
ETest &operator=(const ETest &) = delete;
struct TestCase {
std::string name; // 測試名稱
TestFunction func; // 測試函數(shù)指針 ??
// 使用構(gòu)造函數(shù)初始化列表
TestCase(std::string n, TestFunction f)
: name(std::move(n)), func(std::move(f)) {}
};
std::vector<TestCase> tests_; // 存儲所有測試用例 ??
// 添加測試結(jié)果統(tǒng)計
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