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

More Effective C++ 条款04:非必要不提供默认构造函数

More Effective C++ 条款04:非必要不提供默认构造函数


核心思想默认构造函数并非总是必要的,在某些情况下,强制要求对象在构造时提供必要参数可以创造更安全、更健壮的接口设计,避免对象处于无效状态。

🚀 1. 默认构造函数的利弊分析

1.1 默认构造函数的优势

  • 便于创建数组和标准库容器
  • 简化某些模板代码
  • 支持某些序列化框架

1.2 默认构造函数的劣势

  • 可能允许创建处于无效状态的对象
  • 掩盖了对象的必要初始化要求
  • 增加了接口的模糊性

1.3 问题代码示例

// ❌ 有问题设计:提供默认构造函数但对象可能无效
class Employee {
public:Employee();  // 默认构造Employee(int id, const std::string& name);void setID(int id);void setName(const std::string& name);bool isValid() const;  // 需要检查对象是否有效private:int id_;std::string name_;
};// 使用时的风险
Employee emp;  // 创建了无效对象
// 必须记得调用setter方法,否则对象无效

📦 2. 何时避免默认构造函数

2.1 必需参数缺失时对象无意义的情况

// ✅ 更好设计:强制提供必要参数
class NetworkConnection {
public:// 没有默认构造函数!NetworkConnection(const std::string& address, int port);~NetworkConnection();void sendData(const void* data, size_t size);void receiveData(void* buffer, size_t size);private:std::string address_;int port_;// 连接状态等必需信息
};// 使用:必须提供有效参数
NetworkConnection conn("192.168.1.1", 8080);  // ✅ 有效对象
// NetworkConnection badConn;  // ❌ 编译错误:没有默认构造函数

2.2 不同构造场景的解决方案对比

场景问题解决方案
数组创建Employee employees[10]; 需要默认构造使用指针数组或std::vector
标准库容器vector<Employee> 需要默认构造使用emplace或reserve+push_back
模板代码某些模板需要默认构造使用requires约束或static_assert

⚖️ 3. 解决方案与替代方案

3.1 处理必须使用默认构造的场景

// 方案1:使用指针数组替代对象数组
class Employee {
public:Employee(int id, const std::string& name); // 没有默认构造private:int id_;std::string name_;
};// 创建数组的替代方案
void createEmployeeArray() {// ❌ 不能这样:Employee employees[5];// ✅ 替代方案1:使用指针Employee* employees[5] = {nullptr};employees[0] = new Employee(1, "Alice");// ... 记得手动删除// ✅ 替代方案2:使用vector和emplacestd::vector<Employee> employees;employees.reserve(5);  // 预分配空间employees.emplace_back(1, "Alice");  // 原地构造employees.emplace_back(2, "Bob");// ✅ 替代方案3:使用optional包装std::array<std::optional<Employee>, 5> employeeArray;employeeArray[0] = Employee(1, "Alice");
}

3.2 设计模式应用

// 方案2:使用工厂模式
class EmployeeFactory {
public:static std::unique_ptr<Employee> create(int id, const std::string& name) {return std::make_unique<Employee>(id, name);}// 如果需要"空"对象,提供明确的无效状态static std::unique_ptr<Employee> createInvalid() {// 返回明确标记为无效的对象return std::make_unique<Employee>(-1, "INVALID");}
};// 方案3:使用建造者模式
class EmployeeBuilder {
public:EmployeeBuilder& setId(int id) { id_ = id; return *this; }EmployeeBuilder& setName(const std::string& name) { name_ = name; return *this; }Employee build() const {if (id_ < 0 || name_.empty()) {throw std::invalid_argument("Missing required fields");}return Employee(id_, name_);}private:int id_ = -1;std::string name_;
};// 使用建造者模式
Employee emp = EmployeeBuilder().setId(123).setName("Alice").build();

💡 关键实践原则

  1. 优先考虑对象有效性
    确保对象在构造后立即处于有效状态:

    // ✅ 好设计:构造即有效
    class Date {
    public:Date(int year, int month, int day);  // 验证参数有效性// 没有默认构造函数 - 日期不能"空"
    private:int year_, month_, day_;
    };// ❌ 坏设计:允许无效状态
    class BadDate {
    public:BadDate();  // 创建无效日期// 需要额外方法设置值,期间对象无效
    };
    
  2. 明确表达设计意图
    通过构造函数设计传达业务规则:

    // 业务规则:每个银行账户必须有关联客户
    class BankAccount {
    public:// 强制要求客户信息,避免"无主"账户BankAccount(const Customer& owner, double initialDeposit = 0.0);// 没有默认构造函数!
    };
    
  3. 提供清晰的错误信息
    当缺少必需参数时,在编译期就发现问题:

    // 编译错误比运行时错误更容易发现和修复
    // BankAccount account;  // ❌ 编译错误:没有默认构造函数
    BankAccount account(customer, 100.0);  // ✅ 明确且安全
    

现代C++增强

// 使用Concept约束模板要求
template<typename T>
concept DefaultConstructible = requires {T::T();  // 要求默认构造函数
};// 对于需要默认构造的模板,明确要求
template<DefaultConstructible T>
class Container {// 只能使用有默认构造的类型
};// 使用std::optional处理可能缺失的值
#include <optional>class Configuration {
public:Configuration(const std::string& configPath);  // 必需参数// 但有时可能需要延迟初始化static std::optional<Configuration> fromFile(const std::string& path) {if (/* 文件存在 */) {return Configuration(path);}return std::nullopt;  // 明确表示缺失}
};// 使用std::variant表示多种状态
#include <variant>struct Uninitialized {};
struct Initialized { /* 数据成员 */ };class StatefulObject {
public:StatefulObject() : state_(Uninitialized{}) {}void initialize(const RequiredParams& params) {state_ = Initialized{params};  // 转移到初始化状态}private:std::variant<Uninitialized, Initialized> state_;
};

代码审查要点

  1. 检查每个默认构造函数,确认其创建的对象的有效性
  2. 验证是否有类在缺少必需信息时仍提供了默认构造
  3. 确认数组和容器使用场景都有适当的替代方案
  4. 确保业务规则在构造函数设计中得到正确体现

总结
默认构造函数并非总是必要的设计选择。在许多情况下,避免提供默认构造函数可以创建更安全、更明确的接口,强制使用者在对象构造时提供所有必要信息,从而确保对象始终处于有效状态。虽然这会增加某些使用场景的复杂性(如数组创建、标准库容器使用),但通过智能指针、工厂模式、建造者模式以及现代C++特性如std::optional和std::variant,可以优雅地解决这些问题。在设计类时,应该优先考虑对象的有效性和接口的明确性,而不是为了便利性而提供可能创建无效对象的默认构造函数。

http://www.xdnf.cn/news/1354177.html

相关文章:

  • Day58 Java面向对象13 instanceof 和 类型转换
  • OCR、文档解析工具合集(下)
  • Text2API与Text2SQL深度对比:自然语言驱动的数据交互革命
  • 【51单片机】【protues仿真】基于51单片机冰箱系统
  • 嘉立创EDA快捷键汇总
  • 每日一题8.23
  • Windows应急响应一般思路(三)
  • 从词源和输出生成等角度详细解析PHP中常用文件操作类函数
  • BEVDet/BEVDet4D
  • 【40页PPT】数据安全动态数据脱敏解决方案(附下载方式)
  • LeetCode 分类刷题:2529. 正整数和负整数的最大计数
  • 【大语言模型 16】Transformer三种架构深度对比:选择最适合你的模型架构
  • XCVM1802-2MSEVSVA2197 XilinxAMD Versal Premium FPGA
  • flink常见问题之超出文件描述符限制
  • android studio配置 build
  • VS Code 中创建和开发 Spring Boot 项目
  • JWT实现Token登录验证
  • Nacos-11--Nacos热更新的原理
  • 语义普遍性与形式化:构建深层语义理解的统一框架
  • C++算法题—— 小C的细菌(二维偏序离线 + 树状数组 + 坐标压缩)
  • 使用Proxifier+vmware碰到的一些问题
  • JUC之虚拟线程
  • 论文阅读:Inner Monologue: Embodied Reasoning through Planning with Language Models
  • 173-基于Flask的微博舆情数据分析系统
  • 数据结构 之 【AVL树的简介与部分实现】(部分实现只涉及AVL树的插入问题,包括单旋((右单旋、左单旋))、双旋(左右单旋、右左单旋)等操作)
  • SAP FI 应收应付账龄分析
  • leetcode26:删除有序数组中的重复项Ⅰ(快慢指针解法)
  • X射线胸部肺炎检测:基于深度学习的医学影像分析项目
  • 概率论基础教程第六章 随机变量的联合分布(二)
  • 告别SaaS数据绑架,拥抱数据主权:XK+独立部署版跨境商城定制,为海外物流企业深度赋能