[笔记]Effective C++改善程序与设计的55个具体做法_第四章 设计与声明

条款 18:让接口容易被正确使用,不易被误用。

核心原则:接口设计的核心目标是实现「编译期拦截错误,运行期降低隐患」。借助类型系统、语义约束及自动化资源管理机制,将潜在错误在代码编写阶段彻底消除。

一、促进正确使用:保持一致性与直觉化

  1. 与内置类型行为对齐
  • 案例:自定义Rational类时,为保证与int行为一致,需让a * b = c这一错误写法编译失败,可通过让operator*返回const类型实现(此特性在 C++98 中已支持,C++20 进一步强化了该约束)。

  • 扩展:C++20 引入的consteval关键字能将计算过程转移至编译期,可进一步约束接口误用,例如在编译期检查Rational类的分母不为零。

  1. 遵循 STL 式接口一致性
  • 反例:Java 中数组的length()、字符串的length()与集合的size(),这种接口命名的不一致性增加了使用难度。

  • 正例:C++ 标准库容器统一采用size()获取大小,C++23 相关提案(如std::span)延续了这一设计理念,大幅降低了开发者的记忆成本。

二、阻止误用:依托类型系统与自动化机制

  1. 实施强类型约束
  • 案例Date类中的Month成员,通过explicit Month(int m)结合 C++20 的constexpr进行编译期检查,将其有效值限定在[1,12]范围内,以此替代安全性较弱的enum(C++11 起enum class在一定程度上解决了类型安全问题,但自定义类的灵活性更高)。

  • C++17 扩展:若Month被设计为聚合类型(即没有用户定义的构造函数),需留意 C++17 允许通过继承基类进行聚合初始化,这可能会意外绕过约束。此时,应显式添加=delete的默认构造函数,或标记为explicit以避免此类问题。

  1. 实现资源管理自动化

工厂函数优化

// C++11方案:绑定自定义删除器,规避跨DLL问题
std::shared_ptr<Investment> createInvestment() {
    return {new Stock, getRidOfInvestment}; // 替代原始指针,自动调用删除器
}
  1. 规范参数顺序与语义标记

案例:Date(int year, int month, int day)的参数顺序易混淆,在 C++20 中,可通过struct Year, Month, Day进行强类型包裹,并配合constexpr构造函数解决:

struct Month {
    explicit constexpr Month(int m) : val(m) { if (m < 1 || m > 12) throw std::out_of_range{}; }
    int val;
};
Date(Year y, Month m, Day d); // 可在编译期捕获类型不匹配错误

任何接口如果要求客户必须记得做某件事,就是有着“不正确”使用的倾向,因为客户有可能会忘记那件事。

欲开发一个“容易被正确使用,不容易被误用”的接口,首先必须考虑客户可能做出什么样的错误。

任何接口如果要求客户必须记得做某些事情,就有者“不正确使用”的倾向,因为客户可能会忘记做那件事。

请记住:

  • 好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质
  • “促进正确使用”的办法包括接口的一致性,以及与内置类型的行为的兼容
  • “阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象指,以及消除客户的资源管理责任
  • std::shared_ptr支持定制型删除器(custom deleter)。这可防范DLL问题,可悲用来自动解除互斥锁等等

条款 19:设计 class 犹如设计 type。

设计class需要考虑的问题:

  • 新type的对象应该如何被创建和销毁?
  • 对象的初始化和对象的赋值该有什么样的差别?
  • 新type的对象如果被passed by value(以值传递),意味着什么?
  • 什么是type的”合法值“?
  • 你的新type需要配合某个继承图系(inheritance graph)吗?
  • 你的新type需要什么样的转换?
  • 什么样的操作符和函数对此新type而言是合理的?
  • 什么样的标准函数应该驳回?
  • 谁该取用新type的成员?
  • 什么是新type的“未声明接口”(undeclared interface)
  • 你的type有多么一般化?
  • 你真的需要一个新的type吗?

条款 20:宁以 pass - by - reference - to - const 替换 pass - by - value。

核心考量:在 C++ 中,参数传递方式影响性能与对象语义。值传递因对象拷贝构造和析构,对复杂对象开销巨大;引用传递则可规避此问题,尤其是传递const引用,既能防止对象被意外修改,又能保持高效。

C++98 案例:假设自定义Widget类,有复杂的构造与析构过程。若函数void processWidget(Widget w)采用值传递,每次调用都会触发拷贝构造,即便函数内部未修改w。而改为void processWidget(const Widget& w),仅传递引用,不会产生额外对象拷贝,显著提升性能。

C++11 扩展:C++11 引入右值引用和移动语义,在传递临时对象时,若函数接受右值引用(如void processWidget(Widget&& w)),可实现零拷贝移动,进一步优化性能。但多数情况下,const左值引用仍是通用且安全的选择,既能处理左值参数,又能隐式转换临时右值。

当使用pass-by-value时,如果传递子类对象,会发生对象切割(slicing),因为调用的是基类的构造,子类的相应扩展都会被删除。

当传递的对象是内置类型如int时,pass-by-value往往比pass-by-reference效率要高,这条规则同样适用于STL的迭代器和函数对象。

条款 21:必须返回对象时,别妄想返回其 reference。

返回reference是一个糟糕的设计,会导致内存泄露、不符合预期等糟糕场景发生。

C++98 案例:若函数

const Rational& operator*(const Rational& lhs, const Rational& rhs) {
    Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
    return result;
}

返回局部result的引用,调用者使用时将访问已销毁对象。正确做法是直接return Rational(lhs.n * rhs.n, lhs.d * rhs.d);,编译器在合适场景下会应用 RVO,消除不必要的拷贝。

C++11 及以后:C++11 的移动语义配合 RVO,在返回对象时能实现高效转移。例如,当返回类型支持移动构造时,编译器可将局部对象直接移动到调用者位置,而非传统拷贝。如

std::vector<int> createVector() {
    std::vector<int> v;
    v.push_back(1);
    return v;
}

编译器会将v直接移动到调用者的std::vector中,减少性能损耗。

请记住:

绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。条款4已经为"在单线程环境中合理返回reference指向一个local static对象"提供了一份设计实例。

条款 22:将成员变量声明为 private。

可获得语法一致性,不论是成员还是函数都是以小括号的函数进行调用。

可以对成员的读写进行限制。

可以对成员的值范围进行限制。

将成员变量隐藏在函数接口的背后,可以为“所有可能得实现”提供弹性。

请记住:

  • 切记将成员变量声明为private。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性。
  • protected并不比public更具封装性。

条款 23:宁以 non - member、non - friend 函数替换 member 函数。

主要体现的是一种类的函数组织方式,比较优秀的方式是:

// 头文件"webbrowser.h"--这个头文件针对class WebBrowser自身以及WebBrowser核心技能。
namespace WebBrowserStuff {
    class WebBrowser {...};
    ... // 核心机能,例如几乎所有客户都需要的non-member函数
}

// 头文件"webbrowserbookmarks.h"
namespace WebBrowserStuff {
    ... // 与书签相关的便利函数
}

// 头文件"webbrowsercookies.h"
namespace WebBrowserStuff {
    ... // 与cookie相关的便利函数
}
...

请记住:

  • 宁可拿non-member non-friend函数替代member函数。这样做可以增加封装性、包裹弹性(packaging flexibility)和机能扩充性。

条款 24:若所有参数皆需类型转换,请为此采用 non - member 函数。

举例:

有理数

class Rational {
    ...
};

const Rational operator*(const Rational &lhs, const Rational &rhs)
{
    return Rational(lhs.numerator() * rhs.numerator(), lhs.demominator() * rhs.denominator());
}

Rational oneFourth(1, 4);
Rational result;
result = oneFourth * 2; // 没问题
result = 2 * oneFourth; // Ok

请记住:

如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么在这个函数必须是个non-member。

条款 25:考虑写出一个不抛出异常的 swap 函数。

请记住:

  • 当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。
  • 如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于classes(而非templates),也请特化std::swap。
  • 调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何“命名空间资格修饰”。
  • 为“用户定义类型”进行std templates全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇