当前位置: 首页 > news >正文

CppCon 2016 学习:On using singletons in C++

这段代码实现了一个单例模式(Singleton Pattern),通过指针方式实现单例对象的懒初始化。下面是详细的中文理解和分析:

代码结构解读

class Singleton {Singleton() = default;  // 构造函数私有,防止外部直接实例化
public:static Singleton * instance();  // 获取单例实例的静态方法void foo();                    // 示例成员函数
};
Singleton * Singleton::instance() {static Singleton * inst = nullptr;  // 静态指针,初始为 nullptrif (!inst)                         // 如果还没创建实例inst = new Singleton();        // 通过 new 创建实例(懒初始化)return inst;                       // 返回实例指针
}

关键点说明

  1. 构造函数私有
    • Singleton() 私有,阻止外部直接创建对象,保证只能通过 instance() 获取实例。
  2. 静态成员函数 instance()
    • 通过类名调用,提供访问单例对象的唯一入口。
  3. 静态局部变量 inst 是指针
    • static Singleton* inst = nullptr;
    • 它在第一次调用时初始化为 nullptr,然后指向通过 new 创建的单例对象。
  4. 懒初始化(Lazy Initialization)
    • 只有当第一次调用 instance() 时,单例对象才创建。
    • 这节省了资源,避免程序启动时就创建实例。
  5. 线程安全性问题
    • 这段代码不是线程安全的。多个线程同时调用 instance() 可能导致创建多个实例或竞态条件。
    • 现代 C++ 推荐用 static Singleton inst; 这种方式自动保证线程安全。

优化建议(线程安全单例)

C++11 以后可以写成:

class Singleton {Singleton() = default;
public:static Singleton& instance() {static Singleton inst;  // 静态局部变量自动线程安全初始化return inst;}void foo();
};
  • 优点
    • 线程安全(C++11保证静态局部变量初始化线程安全)
    • 无需动态分配,无需手动管理内存,避免内存泄漏
    • 返回引用避免指针可能引发的空指针问题

总结

  • 你给出的代码是一个经典单例模式实现,使用指针和动态分配实现懒初始化。
  • 需要注意线程安全和内存释放问题。
  • 现代 C++ 推荐用静态局部变量对象的方式实现单例,更简洁安全。

这段代码是单例模式的另一种实现方式,使用了引用而不是指针来返回唯一实例,具体理解如下:

代码解读

class Singleton {Singleton() = default;       // 构造函数私有,防止外部直接实例化
public:static Singleton& instance();  // 返回单例实例的静态方法,返回引用void foo();
};
Singleton& Singleton::instance() {static Singleton inst;       // 静态局部变量,单例实例return inst;                 // 返回实例的引用
}

关键点分析

  1. 构造函数私有
    • 保护单例不被外部直接构造,确保只能通过 instance() 获取实例。
  2. 返回类型是引用 (Singleton&)
    • 保证调用者得到的是实例的引用,而不是指针。
    • 避免了指针的空指针风险,调用更安全。
  3. 静态局部变量 inst
    • 只有第一次调用 instance() 时创建,并且会在程序结束时自动销毁。
    • 这是懒初始化,并且由编译器保证线程安全(C++11及以后)。
  4. 线程安全
    • C++11 标准保证静态局部变量的初始化是线程安全的,因此不需要额外锁机制。
    • 多线程环境中也不会创建多个实例。
  5. 不需要手动释放内存
    • 因为 inst 是静态变量,程序退出时自动销毁,无需手动调用 delete

总结

  • 这是单例模式的推荐实现方法,简单、安全且线程安全。
  • 返回实例的引用,避免了指针的潜在问题。
  • 静态局部变量实现懒初始化,且自动管理生命周期。

单例模式的两种常见实现方法及其用法差异:

1. 指针方式实现(Pointer)

class Singleton {Singleton() = default;
public:static Singleton* instance();void foo();
};
Singleton* Singleton::instance() {static Singleton* inst = nullptr;if (!inst) inst = new Singleton();return inst;
}

调用用法

Singleton::instance()->foo();
  • instance() 返回的是指针,需要用 -> 调用成员函数。
  • 优点:传统写法,灵活。
  • 缺点:可能会出现空指针风险(虽然这里有懒初始化保护),需要手动管理内存(如果没有智能指针的话)。

2. 引用方式实现(Reference)

class Singleton {Singleton() = default;
public:static Singleton& instance();void foo();
};
Singleton& Singleton::instance() {static Singleton inst;return inst;
}

调用用法

Singleton::instance().foo();
  • instance() 返回的是引用,可以直接用.调用成员函数。
  • 优点:语法简洁安全,没有空指针风险,不用担心内存释放。
  • 缺点:无法表示“无实例”的状态。

总结对比

方面指针方式引用方式
返回类型Singleton*Singleton&
成员访问instance()->foo()instance().foo()
内存管理需要手动管理(或智能指针)静态变量自动管理
线程安全需额外考虑C++11后静态局部变量线程安全
语法安全可能空指针无空指针风险
语法简洁稍繁琐简洁

PIMPL(Pointer to IMPLementation) 技术,用于隐藏类的实现细节。总结如下:

PIMPL 模式简介

  • 目的:隐藏类的内部实现细节,减少头文件依赖,提高编译速度,保护接口稳定性,降低类的耦合度。
  • 核心:在类中只声明一个指向实现类的指针(通常是智能指针),真正的实现放在 .cpp 文件里,外部不可见。

你给的示例

class Singleton {class Singleton_impl;                      // 前向声明实现类std::unique_ptr<Singleton_impl> pimpl;    // 指向实现类的智能指针Singleton();
public:static Singleton* instance();void foo();
};
class Singleton::Singleton_impl {
public:void foo();
};
Singleton::Singleton() : pimpl(std::make_unique<Singleton_impl>()) {}
void Singleton::foo() {pimpl->foo();  // 调用实现类的函数
}

关键点解释

  • Singleton_impl 是具体的实现类,定义在 .cpp 里,外部不可访问。
  • pimpl 是持有实现类对象的指针,这里用的是 std::unique_ptr,确保资源自动释放。
  • Singleton::foo() 只是转发调用 Singleton_impl::foo()
  • 这样修改 Singleton_impl 不会影响 Singleton 的接口和使用者的编译,只需重新编译实现文件。

PIMPL 模式的优点

  • 隐藏实现细节,接口更稳定。
  • 减少编译依赖,改动实现不影响使用方。
  • 有助于二进制兼容性,特别适合库开发。
  • 可实现更小的头文件,减少编译时间。

总结

PIMPL 是经典的实现隐藏方案,配合智能指针保证内存安全,尤其在大型项目和库设计中非常实用。

理解了!你展示的是用 抽象基类 + 具体实现类 + 单例模式 结合的设计思想,目的是通过接口隐藏实现细节,同时保证单例的访问。

设计解读

  1. 抽象基类 ISingleton
class ISingleton {
public:virtual void foo() = 0;          // 纯虚函数,定义接口static ISingleton* instance();   // 静态函数,用来获取单例对象指针
};
  • 只声明接口,不暴露具体实现。
  • 用户通过 ISingleton 指针访问,不关心具体类。
  1. 具体实现类 Singleton
class Singleton : public ISingleton {// 具体实现void foo() override { /*...*/ }
};
  • 继承抽象基类,实现具体功能。
  1. 单例实例获取
ISingleton* ISingleton::instance() {static Singleton* inst = nullptr;if (!inst) inst = new Singleton();return inst;
}
  • instance() 返回抽象基类指针,隐藏具体实现类类型。
  • 通过静态局部变量保证单例实例唯一。
  • 延迟初始化,第一次调用创建实例。

整体优点

  • 隐藏实现:客户端只依赖接口 ISingleton,具体实现 Singleton 可以随时替换。
  • 单例保证:实例唯一,方便集中管理。
  • 接口与实现分离:更清晰的设计,更易维护和扩展。

调用示例

ISingleton::instance()->foo();
  • 只需通过接口指针访问,实现细节完全封装。

你展示的这个设计是单例模式的一个封装简化版本,通过隐藏 instance() 的使用,让用户可以直接调用 Singleton::foo(),从而提升易用性。下面是详细解析:

设计目标:简化使用,隐藏 instance() 的存在

你“ditch instance()”的意思是让用户不再手动调用 Singleton::instance()->foo(),而是用更简洁的:

Singleton::foo();  //  更方便、更清晰

结构解析

class Singleton {Singleton() = default;                 // 私有构造,限制实例化void foo_impl();                       // 实际功能实现static Singleton* instance();          // 单例实例获取
public:static void foo();                     // 提供静态接口给用户
};
  • foo_impl()实例方法,逻辑放这里。
  • foo()静态方法,用户调用它就行。
  • foo() 会在内部调用 instance()->foo_impl(),所以用户完全不需要知道 instance() 的存在。

实现细节

void Singleton::foo_impl() {// 实际操作,比如打印、状态处理等
}
Singleton* Singleton::instance() {static Singleton* inst = new Singleton();return inst;
}
void Singleton::foo() {instance()->foo_impl();  // 封装调用
}

优点总结

特性说明
易用性用户只写 Singleton::foo(),无需了解实例
实现封装instance() 是实现细节
单例保障static Singleton* inst 保证全局唯一
控制访问构造函数私有,外部不能构造

类似设计在现实中常见于:

  • 日志系统(Logger::log("...")
  • 全局配置(Config::get("...")
  • 跟踪器、统计器、调试器等工具类

总结一句话

你展示的是一个更“人性化” 的单例封装方式,通过静态成员函数将 instance() 隐藏,让用户调用逻辑更简洁、更清晰,同时保留了单例和封装的优点。非常实用、推荐!

目的:提升使用体验(Ease of use)

通过让用户不直接使用 instance(),而是调用一个静态函数,例如:

Singleton::foo();   //  推荐做法,简洁

来代替下面这些比较啰嗦或暴露实现细节的写法:

Singleton::instance()->foo();  //  指针调用,易错
Singleton::instance().foo();   //  需要返回引用,耦合较强

背后动机(Why ditch instance() in public use?)

  • instance() 是单例实现的内部机制,对用户不必要暴露。
  • 提供一个 统一的静态接口(如 Singleton::foo())使调用者不用关心单例细节。
  • 更符合“最少知识原则”(Law of Demeter):用户不该知道或访问对象结构内部细节

示例代码回顾

class Singleton {Singleton() = default;void foo_impl();                   // 真正的逻辑在这里static Singleton* instance();      // 单例指针(实现细节)
public:static void foo();                 // 推荐给用户用的接口
};
void Singleton::foo() {instance()->foo_impl();            // 静态方法转发到实例方法
}

优点总结

项目解释
简洁Singleton::foo() 更清楚明了
安全避免用户误用裸指针或引用
封装好instance() 成为私有或半私有实现细节
更像工具类使用方式统一,无需理解实例生命周期

实际类比

像很多库中的工具类都采用类似设计:

Logger::log("Something");      //  内部是单例,但用户不知道也不关心
Settings::set("theme", "dark"); //  用户只调用静态接口

总结一句话

你想表达的“ditch instance()”是指让用户只用 Singleton::foo() 这种静态封装方法,不要暴露 instance() 的存在。这是为了提升接口友好性、隐藏实现细节,是一种更现代、实用的单例接口设计方式。

进一步提升易用性:连类都不要了(“ditch instance()”, ditch the class!

你主张:

不仅要避免让用户接触 instance()连类本身都可以不需要,直接用命名空间 + 匿名命名空间实现单例行为。

为什么这么做?

  • 单例的本质只是“程序中全局唯一的一份数据 + 方法”。
  • 如果这个数据只在某个 .cpp 文件内部使用,根本不需要类。
  • 命名空间就可以组织这些方法和数据,不必创建一个对象。

示例代码详解

// 头文件(或外部接口)
namespace Singleton {void foo();  // 提供公开函数接口
}
// 实现文件
namespace Singleton {namespace {// 匿名命名空间中的变量或对象相当于“私有静态成员”int internal_state = 0;// 如果你需要更复杂的状态,也可以放一个 struct 对象在这里struct State {int counter = 0;// ...} state;}void foo() {state.counter += 1;// 逻辑操作...}
}

优点总结

项目解释
更简洁不用写构造函数、instance 方法等
更隐蔽匿名命名空间内的数据完全隐藏在编译单元
更安全无法被外部链接或访问
更易读看起来像普通函数调用,用户不会被“单例”这个实现细节困扰

类 vs 命名空间的对比

特性类(Class Singleton)命名空间(namespace Singleton)
状态存储位置成员变量匿名命名空间内静态变量
接口调用方式Singleton::foo()Singleton::foo()
构造控制(私有等)需要写构造/指针控制不需要,匿名命名空间保证唯一性
单例性明确控制 instance()编译单元唯一性隐式保证

总结一句话:

与其让用户写 Singleton::instance()->foo(),你建议 直接去掉类,用命名空间来封装单例接口和数据,简洁、安全、易用,特别适合无状态或轻状态的全局工具逻辑。

Singleton(单例)模式的代码进行测试,并在不同的实现方式下给出测试支持的策略。

测试单例的一些挑战(Testing Singleton)

普通单例难以测试的原因:

  • 状态是全局且持久的,一次初始化就无法回退
  • 单例隐藏在 instance() 方法后,难以替换或 mock
  • 如果依赖的状态不可控,测试就不再“纯粹”。
    添加测试辅助接口:
void clearState(); // only for unit-tests!
  • 为了支持测试,可以给 Singleton 加一个 clearState() 方法来重置内部状态。
  • 这是一种妥协手段,用来保证测试隔离性。
  • 缺点是:
    • 会暴露额外接口。
    • 如果不小心在生产代码中使用,会引发逻辑错误。
#define private public
  • 在测试中临时改写访问权限,来访问本应私有的成员。
  • 非常危险,强烈不推荐!
  • 会破坏封装性,引发未定义行为,并让测试代码依赖实现细节,极难维护。
    请不要这么做。
    使用抽象基类(接口注入):
class ISingleton {
public:virtual void foo() = 0;virtual void clearState() = 0; // for test
};
class Singleton : public ISingleton {// 具体实现
};
  • 在测试中可以使用 mock 实现来替换真正的 Singleton
  • 这是最常见的测试友好方式:面向接口编程
  • 缺点是略显笨重,对简单逻辑显得过度设计。
    使用无类实现方式来简化和隔离测试:
namespace Singleton {namespace detail {// 内部数据}void foo();void clearState(); // test helper
}
  • 使用匿名命名空间中的数据来封装“全局状态”。
  • 提供公开的测试辅助函数如 clearState()
  • 整体设计保持清晰,测试也变得更容易,没有 class 成员访问权限限制的问题

总结

方法是否推荐优缺点
添加 clearState()适度推荐简单直接,但需小心接口污染
#define private public禁止使用极其脆弱,不可维护
使用抽象基类推荐面向接口编程,适合复杂逻辑
无类命名空间实现推荐简洁封装,可控状态,易于测试
http://www.lryc.cn/news/571548.html

相关文章:

  • 14.2 《3小时从零搭建企业级LLaMA3语言助手:GitHub配置+私有化模型集成全实战》
  • Uniapp性能优化全面指南:从原理到实践
  • 从0开始学习R语言--Day26--因果推断
  • 4. 时间序列预测的自回归和自动方法
  • Docker学习笔记:数据卷
  • 秋招是开发算法一起准备,还是只准备一个
  • 【CUDA编程】OptionalCUDAGuard详解
  • 【6G技术探索】MCP协议整理分享
  • 6.IK分词器拓展词库
  • # 我使用过的 HTML + CSS 实践总结笔记(含说明)
  • 设计模式笔记_创建型_工厂模式
  • 九日集训第六天
  • 【AI News | 20250617】每日AI进展
  • Tomcat本地部署Maven Java Web项目
  • 从C++编程入手设计模式——策略设计模式
  • uniapp 对接deepseek
  • 手术麻醉系统源码 手麻系统源码 Java手术室管理系统源码
  • 2025年渗透测试面试题总结-红队攻防工程师(题目+回答)
  • 缓存系统-基本概述
  • Ajax 核心知识点全面总结
  • 前端开发面试题总结-vue2框架篇(三)
  • 网络层协议 IP 协议介绍 -- IP 协议,网段划分,私有 IP 和 公网 IP,路由
  • KingbaseES 在线体验平台深度评测
  • 计算机硬件——外设、其他部件
  • CentOS7 安装最新版 Docker
  • 【MySQL】MySQL 数据库操作与设计
  • 【系统设计【4】】设计一个限流器:从理论到实践的完整解决方案
  • 从C++编程入手设计模式——外观模式
  • AI智能体应用市场趋势分析
  • Black自动格式化工具