More Effective C++ 条款04:非必要不提供默认构造函数
核心思想 :默认构造函数并非总是必要的,在某些情况下,强制要求对象在构造时提供必要参数可以创造更安全、更健壮的接口设计,避免对象处于无效状态。
🚀 1. 默认构造函数的利弊分析
1.1 默认构造函数的优势:
- 便于创建数组和标准库容器
- 简化某些模板代码
- 支持某些序列化框架
1.2 默认构造函数的劣势:
- 可能允许创建处于无效状态的对象
- 掩盖了对象的必要初始化要求
- 增加了接口的模糊性
1.3 问题代码示例:
cpp
// ❌ 有问题设计:提供默认构造函数但对象可能无效
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 必需参数缺失时对象无意义的情况:
cpp
// ✅ 更好设计:强制提供必要参数
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 处理必须使用默认构造的场景:
cpp
// 方案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和emplace
std::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 设计模式应用:
cpp
// 方案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();
💡 关键实践原则
-
优先考虑对象有效性
确保对象在构造后立即处于有效状态:
cpp// ✅ 好设计:构造即有效 class Date { public: Date(int year, int month, int day); // 验证参数有效性 // 没有默认构造函数 - 日期不能"空" private: int year_, month_, day_; }; // ❌ 坏设计:允许无效状态 class BadDate { public: BadDate(); // 创建无效日期 // 需要额外方法设置值,期间对象无效 };
-
明确表达设计意图
通过构造函数设计传达业务规则:
cpp// 业务规则:每个银行账户必须有关联客户 class BankAccount { public: // 强制要求客户信息,避免"无主"账户 BankAccount(const Customer& owner, double initialDeposit = 0.0); // 没有默认构造函数! };
-
提供清晰的错误信息
当缺少必需参数时,在编译期就发现问题:
cpp// 编译错误比运行时错误更容易发现和修复 // BankAccount account; // ❌ 编译错误:没有默认构造函数 BankAccount account(customer, 100.0); // ✅ 明确且安全
现代C++增强:
cpp// 使用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_; };
代码审查要点:
- 检查每个默认构造函数,确认其创建的对象的有效性
- 验证是否有类在缺少必需信息时仍提供了默认构造
- 确认数组和容器使用场景都有适当的替代方案
- 确保业务规则在构造函数设计中得到正确体现
总结:
默认构造函数并非总是必要的设计选择。在许多情况下,避免提供默认构造函数可以创建更安全、更明确的接口,强制使用者在对象构造时提供所有必要信息,从而确保对象始终处于有效状态。虽然这会增加某些使用场景的复杂性(如数组创建、标准库容器使用),但通过智能指针、工厂模式、建造者模式以及现代C++特性如std::optional和std::variant,可以优雅地解决这些问题。在设计类时,应该优先考虑对象的有效性和接口的明确性,而不是为了便利性而提供可能创建无效对象的默认构造函数。