Effective C++ 条款19:设计class犹如设计type
核心思想 :设计新的class时,应当像语言设计者设计内置类型一样慎重,考虑对象的创建、销毁、初始化、拷贝、类型转换等所有方面。
⚠️ 1. 类设计的关键问题域
对象生命周期管理:
cpp
class ResourceHandle {
public:
// 构造和析构:资源如何获取?如何释放?
ResourceHandle(const std::string& resId);
~ResourceHandle(); // 需要释放资源吗?
private:
Resource* resource_;
};
值语义与行为:
cpp
class Rational {
public:
// 拷贝操作:允许拷贝吗?浅拷贝还是深拷贝?
Rational(const Rational& other);
Rational& operator=(const Rational& other);
// 类型转换:支持隐式转换吗?
operator double() const; // 危险:可能非预期转换
};
🚨 2. 解决方案:系统化设计方法
明确对象创建方式:
cpp
class Session {
public:
// 静态工厂方法:控制创建逻辑
static Session createFromNetwork();
static Session createFromFile(const std::string& path);
// 禁用拷贝
Session(const Session&) = delete;
Session& operator=(const Session&) = delete;
private:
Session(); // 私有构造
};
安全类型转换接口:
cpp
class SafeRational {
public:
// 显式转换函数(C++11)
explicit operator double() const {
return static_cast<double>(numerator)/denominator;
}
// 转换运算符替代方案
double toDouble() const { /* ... */ } // 更安全的显式转换
};
⚖️ 3. 关键设计原则与决策
设计维度 | 关键问题 | 推荐实践 |
---|---|---|
对象创建/销毁 | 构造函数参数?析构函数必要性? | RAII模式管理资源 |
初始化/赋值区别 | 构造函数与赋值操作符行为是否一致? | 确保一致性 |
值传递方式 | pass-by-value是否高效? | 小对象传值,大对象传const引用 |
操作符重载 | 哪些操作符需要重载? | 仅重载符合直觉的操作符 |
类型转换控制 | 是否允许隐式转换? | 使用explicit 禁止非预期转换 |
成员访问权限 | 哪些成员公开?哪些需要保护? | 最小化public接口 |
继承体系设计 | 是否作为基类?虚函数如何设计? | 明确声明final 或override |
模板泛化可能性 | 是否应设计为类模板? | 评估未来需求 |
标准库兼容性 | 是否满足STL容器要求? | 提供必要的类型特征 |
成员函数设计规范:
cpp
class Polynomial {
public:
// 常量成员函数:不修改对象状态
double evaluate(double x) const noexcept;
// 异常安全保证
void normalize() &; // 仅限左值对象调用
// 引用限定符(C++11)
void process() &&; // 仅限右值对象调用
};
继承体系设计规范:
cpp
// 接口类设计
class Drawable {
public:
virtual void draw() const = 0;
virtual ~Drawable() = default;
// 禁止拷贝(接口类通常不可拷贝)
Drawable(const Drawable&) = delete;
Drawable& operator=(const Drawable&) = delete;
};
// 具体实现类
class Circle final : public Drawable {
public:
void draw() const override; // 明确重写
// ... // 禁止进一步继承(final)
};
💡 关键原则总结
- 生命周期全周期设计
- 构造/析构:资源获取即初始化(RAII)
- 拷贝控制:明确
=default
/=delete
拷贝操作
- 类型行为一致性
- 操作符重载:行为需符合内置类型预期
- 类型转换:优先使用
explicit
和命名转换函数
- 接口最小化原则
- 成员函数:提供完备但最小的操作集合
- 访问控制:严格限制
private
/protected
- 继承体系明确性
- 基类:声明虚析构函数,明确抽象接口
- 派生类:使用
final
/override
明确意图
危险类设计示例:
cppclass AutoPtr { // 已废弃的auto_ptr问题 public: // 问题1:允许从临时对象构造 AutoPtr(AutoPtr& other); // 非const引用 // 问题2:转移所有权但不明确 AutoPtr& operator=(AutoPtr& other); // 问题3:支持隐式指针转换 operator void*() const; // 可能导致误用 };
安全重构方案:
cpp// 解决方案:现代unique_ptr设计理念 template<typename T> class UniquePtr { public: // 明确所有权转移语义 UniquePtr(UniquePtr&& other) noexcept; // 移动构造 UniquePtr& operator=(UniquePtr&& other) noexcept; // 移动赋值 // 禁止拷贝 UniquePtr(const UniquePtr&) = delete; UniquePtr& operator=(const UniquePtr&) = delete; // 显式bool转换(安全) explicit operator bool() const noexcept; // 明确资源释放接口 void reset() noexcept; T* release() noexcept; };