学完Efficient c++ (18-20)

27 篇文章 0 订阅
24 篇文章 0 订阅
本文讨论了如何通过设计合理的接口、使用外覆类型、语法限制、一致性与一致性、以及避免对象切片等策略,帮助开发者设计易于正确使用且不易误用的接口,减少调用者的错误可能性。
摘要由CSDN通过智能技术生成

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

本条款告诉你如何帮助你的客户在使用你的接口时避免他们犯错误

在设计接口时,我们常常会错误地假设,接口的调用者拥有某些必要的知识来规避一些常识性的错误。但事实上,接口的调用者并不总是像正在设计接口的我们一样“聪明”或者知道接口实现的”内幕信息“,结果就是我们错误的假设使接口表现得不稳定。这些不稳定因素可能是由于调用者缺乏某些先验知识,也有可能仅仅是代码上的粗心错误。接口的调用者可能是别人,也可能是未来的你。所以一个合理的接口,应该尽可能的从语法层面并在编译之时运行之前,帮助接口的调用者规避可能的风险。

  • 使用外覆类型(wrapper)提醒调用者传参错误检查,将参数的附加条件限制在类型本身

当调用者试图传入数字“13”来表达一个“月份”的时候,你可以在函数内部做运行期的检查,然后提出报警或一个异常,但这样的做法更像是一种责任转嫁——调用者只有在尝试过后才发现自己手残把“12”写成了“13”。如果在设计参数类型时就把“月份”这一类型抽象出来,比如使用enum class(强枚举类型),就能帮助客户在编译时期就发现问题,把参数的附加条件限制在类型本身,可以让接口更易用。

  • 语法层面限制调用者不能做的事

接口的调用者往往无意甚至没有意识到自己犯了个错误,所以接口的设计者必须在语法层面做出限制。一个比较常见的限制是加上const,比如在operate*的返回类型上加上const修饰,可以防止无意错误的赋值if (a * b = c)

  • 接口应表现出与内置类型的一致性

让自己的类型和内置类型的一致性,比如自定义容器的接口在命名上和STL应具备一致性,可以有效防止调用者犯错误。或者你有两个对象相乘的需求,那么你最好重载operator*而并非设计名为”multiply”的成员函数。

  • 从语法层面限制调用者必须做的事

别让接口的调用者总是记得做某些事情,接口的设计者应在假定他们总是忘记这些条条框框的前提下设计接口。比如用智能指针代替原生指针就是为调用者着想的好例子,尽量使用智能指针,避免跨DLL的 new 和 delete,使用智能指针自定义删除器来解除互斥锁(mutexes)。如果一个核心方法需要在使用前后设置和恢复环境(比如获取锁和归还锁),更好的做法是将设置和恢复环境设置成纯虚函数并要求调用者继承该抽象类,强制他们去实现。在核心方法前后对设置和恢复环境的调用,则应由接口设计者操心。

当方法的调用者(我们的客户)责任越少,他们可能犯的错误也就越少。

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

本条款提醒我们设计class需要注意的细节,但并没有给每一个细节提出解决方案,只是提醒而已。每次设计class时最好在脑中过一遍以下问题:

  • 对象该如何创建销毁:包括构造函数、析构函数以及new和delete操作符的重构需求。
  • 对象的构造函数与赋值行为应有何区别:构造函数和赋值操作符的区别,重点在资源管理上。(见条款4)
  • 对象被拷贝时应考虑的行为:拷贝构造函数。
  • 对象的合法值是什么?最好在语法层面、至少在编译前应对用户做出监督。
  • 新的类型是否应该复合某个继承体系,这就包含虚函数的覆盖问题。
  • 新类型和已有类型之间的隐式转换问题,这意味着类型转换函数和非explicit函数之间的取舍。
  • 新类型是否需要重载操作符。
  • 什么样的接口应当暴露在外,而什么样的技术应当封装在内(public和private)
  • 新类型的效率、资源获取归还、线程安全性和异常安全性如何保证。
  • 这个类是否具备template的潜质,如果有的话,就应改为模板类。

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

函数接口应该以const引用的形式传参,而不应该是按值传参,否则可能会有以下问题:

  • 当使用按值传参时,程序会调用对象的拷贝构造函数构建一个在函数内作用的局部对象,涉及大量参数的复制,这些副本大多是没有必要的,这个过程的开销可能会较为昂贵。对于任何用户自定义类型,使用按常引用传参是较为推荐的(因为没有任何新对象被创建,这种传参方式不会调用任何构造函数或析构函数,所以效率比按值传参高得多。):
    bool ValidateStudent(const Student& s);
  • 如果拷贝构造函数设计的是深拷贝而非浅拷贝,那么拷贝的成本将远远大于拷贝某几个指针。
  • 对于多态而言,将父类设计成按值传参,如果传入的是子类对象,仅会对子类对象的父类部分进行拷贝,即部分拷贝,而所有属于子类的特性将被丢弃(对象切割),造成不可预知的错误,同时虚函数也不会被调用。
class Window {
public:
    ...
    std::string GetName() const;
    virtual void Display() const;
};

class WindowWithScrollBars : public Window {
public:
    virtual void Display() const override;
};
void PrintNameAndDisplay(Window w) {    // 按值传参,会发生对象切片
    std::cout << w.GetName();
    w.Display();
}

此处按值传参时,调用了基类Window的拷贝构造函数而非派生类的拷贝构造函数,因此在函数种使用的是一个Window对象,调用虚函数时也只能调用到基类的虚函数Window::Display

由于按引用传递不会创建新对象,这个问题就能得到避免:

void PrintNameAndDisplay(const Window& w) { // 参数不会被切片
    std::cout << w.GetName();
    w.Display();
}
  • 小的类型并不意味着按值传参的成本就会小。首先,类型的大小与编译器的类型和版本有很大关系,某些类型在特定编译器上编译结果会比其他编译器大得多。小的类型也无法保证在日后代码复用和重构之后,其类型始终很小。
  • 对于内置类型、STL的迭代器和函数对象,使用按值传参是比较合适的。
  • 18
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值