博客 阅读时间 5 分钟

最常用的 7 个 C++ 惯用法(第 1 部分)

分享本文
Top 7 most used C++ idioms (Part1).

惯用法(idiom)和设计模式都为软件开发中反复出现的问题提供了常见的解决方案,但它们在适用范围、粒度和正式程度上有所不同:

  1. 适用范围:
    • 惯用法:惯用法是小型的、语言特定的编码技术或模式,用于解决特定编程语言中的具体编程挑战。它们通常涉及利用语言特性或约定,以高效且有效的方式达成期望的结果。
    • 设计模式:设计模式是更高层次的、与语言无关的架构性解决方案,针对软件工程中常见的设计问题。它们为解决设计问题和推广软件设计最佳实践提供通用的可复用模板。
  2. 粒度:
    • 惯用法:惯用法往往粒度更细,专注于单一编程语言内的特定编码结构或技术。它们通常涉及操纵语言特性或语法以达成特定目标。
    • 设计模式:设计模式更加全面,处理软件系统内更宽泛的设计概念和组件之间的关系。它们提供在更高抽象层次上组织和构建代码的模板。
  3. 正式程度:
    • 惯用法:惯用法通常是非正式的,一般在特定编程社区内通过经验、代码审查或编程文献传播。它们不一定总是有正式的名称或文档。
    • 设计模式:设计模式是更正式、文档完善的常见设计问题解决方案。它们通常有公认的名称、描述和实现指南,收录在"四人组"(GoF)的《设计模式:可复用面向对象软件的基础》等文献中。

在本文中,我们将尝试探究这些常见的 C++ 惯用法:

  1. 1 - RAII
  2. 2 - Pimpl
  3. 3 - 奇异递归模板模式(CRTP)
  4. 4 - 拷贝并交换(Copy-and-swap)
  5. 5 - 类型擦除(Type Erasure)
  6. 6 - 非虚接口(NVI)
  7. 7 - SFINAE(替换失败不是错误)

第一部分将重点介绍 RAII 和 Pimpl 惯用法:

RAII(资源获取即初始化)

在 C++ 中,最常用的惯用法之一是 RAII(资源获取即初始化)。RAII 是一种强大且被广泛采用的技术,用于以确定性的、异常安全的方式管理内存、文件句柄、网络连接和锁等资源。它将资源的生命周期与对象的生命周期绑定,确保资源被正确地获取和释放。

以下是 RAII 的工作方式及其常见使用场景:

1-内存管理:RAII 在 C++ 中被广泛用于使用指针管理动态内存分配,特别是与 C++ 标准库中的std::unique_ptrstd::shared_ptr配合使用。这些智能指针类确保当包含指针的对象离开作用域时,内存会被自动释放。

{
    std::shared_ptr<int> ptr(new int); // Resource acquired
    // Use ptr
} // Resource released automatically when ptr goes out of scope

2-文件处理:RAII 用于管理文件资源,确保文件在使用后被正确关闭,即使存在异常或提前返回的情况。

{
    std::ifstream file("example.txt"); // Resource acquired
    // Read from file
} // Resource released automatically when file goes out of scope

3-锁机制:RAII 用于管理锁,确保代码的关键部分被正确同步,并确保锁在不再需要时被释放。

{
    std::ifstream file("example.txt"); // Resource acquired
    // Read from file
} // Resource released automatically when file goes out of scope

4-自定义类中的资源管理:开发者经常在自己的类中实现 RAII,以管理数据库连接、网络套接字和 GPU 资源等自定义资源。

RAII 提高了资源安全性,降低了资源泄漏和悬空指针的可能性,并通过将资源与对象生命周期绑定来简化资源管理。它被认为是现代 C++ 编程的基石之一,在整个语言及其标准库中被广泛使用。

Pimpl

Pimpl 惯用法("指向实现的指针",Pointer to Implementation)是一种 C++ 设计技术,用于向类的使用者隐藏其实现细节。它有助于改善封装、减少编译期依赖,并在实现细节变化时最大限度地减少重新编译。

Pimpl 惯用法的工作方式如下:

  1. 接口与实现的分离:使用 Pimpl 惯用法时,类的公共接口定义在其头文件中,而私有实现细节则封装在单独的实现文件中。这种分离使类的使用者只与其公共接口交互,隐藏了实现的复杂性。
  2. 指向实现的指针:类声明中不直接包含实现细节,而是包含一个指向不透明的(即前置声明的)实现类的指针。这个指针通常声明为类的私有成员。
  3. 前置声明:由于实现细节对类的使用者隐藏,头文件中只需要实现类的声明。这通过前置声明(class Impl;)来实现,避免了在头文件中包含实现细节的需要。
  4. 减少编译依赖:通过分离接口与实现并使用前置声明,对实现细节的修改(例如添加、删除或修改私有成员)不需要重新编译使用该类接口的代码。这可以显著缩短构建时间,在大型代码库中尤其如此。
  5. 动态内存分配:通常,实现类使用new在堆上动态分配,指向它的指针由公共类管理。这支持动态多态,并使得"桥接"和"策略"设计模式的使用成为可能。

下面是一个演示 Pimpl 惯用法的简化示例:

// MyClass.h (Header file)
#pragma once

class MyClass {
public:
    MyClass();
    ~MyClass();
    
    void doSomething();
    
private:
    class Impl; // Forward declaration
    
    Impl* pImpl; // Pointer to implementation
};
// MyClass.cpp (Implementation file)
#include "MyClass.h"

class MyClass::Impl {
public:
    void doInternalWork() {
        // Implementation details...
    }
};

MyClass::MyClass() : pImpl(new Impl()) {}

MyClass::~MyClass() {
    delete pImpl;
}

void MyClass::doSomething() {
    pImpl->doInternalWork();
}

通过使用 Pimpl 惯用法,MyClass的实现细节对类的使用者隐藏,对实现的修改不会影响客户端代码,也不需要重新编译依赖文件。这提升了 C++ 项目的模块化、可维护性和构建速度。

最常用的 7 个 C++ 惯用法(第 2 部分)

分享本文