实战演练:用现代 C++ 重构一个“老项目”

学了这么多特性,你是否感觉"学是学了,但不知道怎么用"?纸上谈兵终觉浅,绝知此事要躬行。本文将选取一个典型的"老项目"片段,用我们学过的现代 C++ 特性进行一次彻底的重构。


系列文章索引

  • 第 1 篇: 《告别"祖传C++":开启你的现代C++之旅》
  • 第 2 篇: 《现代C++的基石:你不得不知的C++11/14/17核心特性》
  • 第 3 篇: 《C++20 Concepts:让模板错误信息不再"天书"》
  • 第 4 篇: 《C++20 Ranges:告别手写循环,像 SQL 一样操作数据》
  • 第 5 篇: 《C++20 协程初探:用同步思维写异步代码》
  • 第 6 篇: 《C++20 Modules:终结"头文件地狱"的曙光》
  • 第 7 篇: 《尝鲜C++23:std::mdspan、std::expected与更多实用利器》
  • (本文)第 8 篇: 《实战演练:用现代 C++ 重构一个"老项目"》
  • 第 9 篇: 《现代 C++ 最佳实践清单:编写更安全、更高效的代码》

0. 前言:从理论到实践的鸿沟

在前面的文章中,我们学习了从 C++11 到 C++23 的众多强大特性。但理论知识如何转化为实际的代码质量提升?这是许多开发者面临的共同问题。

本文将通过一个具体的案例------一个简单的日志处理器------来弥合这道鸿沟。你将直观地看到,现代 C++ 的特性如何像一套组合拳,系统性地提升代码在安全性、可读性、可维护性和性能上的全方位表现。

1. 项目背景:"祖传"的日志处理器

假设我们有一个古老的日志系统,它用 C 风格和旧 C++ 混合写成,充满了各种"坏味道"。

legacy_logger.h

cpp 复制代码
#pragma once
#include <string>

enum LogLevel {
    INFO,
    WARNING,
    ERROR
};

struct LogEntry {
    LogLevel level;
    std::string message;
};

legacy_logger.cpp

cpp 复制代码
#include "legacy_logger.h"
#include <cstdio>
#include <ctime>
#include <sstream>

class LegacyLogger {
public:
    LegacyLogger(const std::string& filename) {
        // 手动管理资源,危险!
        file_ = fopen(filename.c_str(), "a");
        if (!file_) {
            // 简陋的错误处理
            perror("Failed to open log file");
        }
    }

    ~LegacyLogger() {
        // 记得手动释放,但如果构造函数中途失败呢?
        if (file_) {
            fclose(file_);
        }
    }

    void log(const LogEntry& entry) {
        if (!file_) return;

        // 不安全的 C 风格格式化
        char buffer[1024];
        time_t now = time(0);
        strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", localtime(&now));

        const char* level_str = (entry.level == ERROR) ? "ERROR" :
                                (entry.level == WARNING) ? "WARNING" : "INFO";
        
        // 使用 fprintf,如果 entry.message 很长,可能被截断
        fprintf(file_, "[%s] %s: %s\n", buffer, level_str, entry.message.c_str());
        fflush(file_); // 每次都刷新,可能影响性能
    }

private:
    FILE* file_; // 原始资源句柄
};

痛点分析:

  • 资源不安全FILE* 手动管理,如果构造函数中 fopen 后的其他操作抛出异常,file_ 就会泄漏。
  • 缓冲区风险 :使用固定大小的 char 数组和 strftime/fprintf,存在潜在的缓冲区溢出和截断风险。
  • 代码冗余:日志级别到字符串的转换逻辑是手写的,如果增加新级别,需要修改多处代码。
  • 难以扩展 :如果需要按日志级别过滤,或者改变输出格式,需要侵入 log 函数内部进行修改。

2. 重构第一步:用 RAII 和智能指针保障安全

首先,我们解决最核心的资源安全问题。

cpp 复制代码
#include <fstream>   // 使用 RAII 的文件流
#include <memory>    // 虽然这里用不上,但这是 RAII 的核心
#include <string>

class ModernLogger {
public:
    // std::ofstream 在析构时自动关闭文件,异常安全
    explicit ModernLogger(const std::string& filename) 
        : file_stream_(filename, std::ios::app) {}

    void log(const LogEntry& entry) {
        if (!file_stream_) return;
        
        // ... 实现细节在下面 ...
    }

private:
    std::ofstream file_stream_; // RAII 管理文件生命周期
};

改进:

  • 使用 std::ofstream 替代 FILE*。无论函数如何退出(正常返回或异常),文件都会被正确关闭。资源安全得到了根本性保障。

3. 重构第二步:用 C++20 Ranges 简化日志过滤

现在,我们增加一个新需求:从一堆日志中,筛选出所有 ERROR 级别的日志。

旧式实现:

cpp 复制代码
std::vector<LogEntry> filter_errors_old(const std::vector<LogEntry>& logs) {
    std::vector<LogEntry> errors;
    for (const auto& entry : logs) {
        if (entry.level == ERROR) {
            errors.push_back(entry);
        }
    }
    return errors;
}

现代 C++20 Ranges 实现:

cpp 复制代码
#include <ranges>
#include <algorithm>

// C++23 的 std::ranges::to 更简洁,这里用 C++20 的辅助函数
template<std::ranges::range R>
auto to_vector(R&& r) {
    std::vector<std::ranges::range_value_t<R>> v;
    for (auto&& e : r) {
        v.push_back(std::forward<decltype(e)>(e));
    }
    return v;
}

std::vector<LogEntry> filter_errors_modern(const std::vector<LogEntry>& logs) {
    return to_vector(logs | std::views::filter([](const LogEntry& e) {
        return e.level == ERROR;
    }));
}

改进:

  • 代码从命令式变为声明式。我们"声明"了要做什么(过滤错误),而不是"描述"如何做(循环、判断、push_back)。
  • 可读性极高,逻辑一目了然。
  • 没有中间变量,性能更优。

4. 重构第三步:用 Concepts 和 Lambda 增强扩展性

最后,我们希望日志的格式是可定制的。例如,有时需要 JSON 格式,有时需要纯文本格式。

旧式实现(虚函数):

需要定义一个 Formatter 基类和多个子类,通过继承和多态实现。

现代 C++ 实现:

我们可以定义一个 Formatter Concept,然后用任何满足该 Concept 的类型(包括 Lambda)来格式化日志。

cpp 复制代码
#include <concepts>

// 定义一个 Formatter Concept
template<typename F>
concept Formatter = requires(F formatter, const LogEntry& entry) {
    { formatter(entry) } -> std::convertible_to<std::string>;
};

// 让 log 函数接受任何 Formatter
template<Formatter F>
void ModernLogger::log(const LogEntry& entry, F formatter) {
    if (!file_stream_) return;
    file_stream_ << formatter(entry) << std::endl;
}

// 使用时,我们可以用 Lambda 就地创建格式化器
int main() {
    ModernLogger logger("app.log");
    LogEntry entry{ERROR, "Database connection failed"};

    // 纯文本格式
    auto text_formatter = [](const LogEntry& e) {
        // 使用 std::format (C++20) 更安全,这里用简化版
        return "[" + std::to_string(e.level) + "] " + e.message;
    };
    logger.log(entry, text_formatter);

    // JSON 格式
    auto json_formatter = [](const LogEntry& e) {
        return R"({"level": ")" + std::to_string(e.level) + R"(", "message": ")" + e.message + "\"}";
    };
    logger.log(entry, json_formatter);
}

改进:

  • 极致的灵活性 :不再需要继承体系,任何满足 Formatter Concept 的可调用对象(Lambda、函数对象)都可以作为格式化器。
  • 代码内聚:格式化逻辑与使用它的地方紧挨着,易于理解和维护。
  • 类型安全:Concepts 在编译期就保证了传入的格式化器是可用的。

5. 重构前后对比:代码会说话

维度 "祖传"代码 现代 C++ 代码
安全性 低(手动资源管理,缓冲区溢出风险) 高(RAII,类型安全的格式化)
可读性 低(逻辑混杂,命令式风格) 高(声明式,意图清晰)
可维护性 低(硬编码,修改一处影响多处) 高(模块化,高内聚低耦合)
可扩展性 低(需要修改核心类,依赖继承) 高(基于 Concepts,零成本抽象)
性能 中(可能有不必要的拷贝,频繁 I/O 刷新) 高(Ranges 零开销抽象,可定制 I/O 策略)

6. 总结与展望

通过这次实战,我们看到现代 C++ 不仅仅是语法的堆砌,而是一套能够系统性地提升代码质量的组合拳。从 RAII 保障资源安全,到 Ranges 简化数据处理,再到 Concepts 提供极致的灵活性,每一项特性都在解决一个具体的痛点。

重构是一个持续的过程。在你的日常工作中,可以尝试用"小步快跑"的方式,逐步将旧代码现代化。比如,下次遇到一个裸指针,就把它换成 std::unique_ptr;遇到一个复杂的 for 循环,就尝试用 Ranges 重构。

现在,审视你自己的项目,找到最让你头疼的一块"代码硬骨头",尝试用今天学到的技巧去啃下它!

在评论区分享你的重构成果吧!

相关推荐
草莓熊Lotso3 小时前
unordered_map/unordered_set 使用指南:差异、性能与场景选择
java·开发语言·c++·人工智能·经验分享·python·网络协议
咔咔咔的5 小时前
1930. 长度为 3 的不同回文子序列
c++
Cinema KI10 小时前
吃透C++继承:不止是代码复用,更是面向对象设计的底层思维
c++
Dream it possible!13 小时前
LeetCode 面试经典 150_二叉搜索树_二叉搜索树中第 K 小的元素(86_230_C++_中等)
c++·leetcode·面试
Bona Sun14 小时前
单片机手搓掌上游戏机(十四)—pico运行fc模拟器之电路连接
c语言·c++·单片机·游戏机
oioihoii14 小时前
性能提升11.4%!C++ Vector的reserve()方法让我大吃一惊
开发语言·c++
小狗爱吃黄桃罐头15 小时前
《C++ Primer Plus》模板类 Template 课本实验
c++
码力码力我爱你17 小时前
Harmony OS C++实战
开发语言·c++
Vect__17 小时前
别再只懂 C++98!C++11 这7个核心特性,直接拉开你与普通开发者的差距
c++