4. 设计与声明

18. 让接口容易被正确使用,不易被误用

1
2
3
4
class Date{
public:
Date(int month, int day, int year);
};
  1. 函数接口 可能以错误的次序传递参数,或者传递一个无效的月份或天数
  2. 导入简单的外覆类型来区别天数,月份等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct Day{
explicit Day(int d)
:val(d){}
int val;
}

struct Month{
explicit Month(int m)
:val(m){}
int val;
}

struct Year{
explicit Year(int y)
:val(y){}
int val;
}
  1. 令Day,Month,Year成为成熟且经充分锻炼的类并封装内部数据,比简单使用structs好
  2. 限制正确的类型的取值,比较安全的方法时预定义所有有效的Months,以函数替换对象,表现某个特定月份,因为non-local static对象的初始化次序可能出现问题

  1. 预防客户错误的另一个办法是,限制类型内什么是可做,什么是不能做。常见的限制是加上const。条款3:以const修饰operator*的返回类型可阻止客户因用户自定义类型而犯错
  2. 除非有好理由,否则应该尽量令你的types的行为于内置types一致
  3. 避免无端与内置类型不兼容,真正的理由是为了提供行为一致的接口。STL容器的接口十分一致,这使得它们非常容易被调用
  4. 较佳接口的设计原则是令函数返回一个智能指针,事实上,返回tr1::shared_ptr让接口设计者阻止资源泄露的错误
1
std::tr1::shared_ptr<Investment> createInvestment();
  1. 假设类设计者期许"从createInvestment取得Investment*指针"的客户将该指针传递给一个名为getRidOfInvestment的函数,而不是直接使用delete。这样一个接口又开启通往另一个客户错误的大门,该错误是"企图使用错误的资源析构机制",设计者可以设计一个返回将getRidOfInvestment绑定为删除器"的tr1::shared_ptr
  2. tr1::shared_ptr提供的某个构造函数接受两个实参:一个是被管理的指针,另一个是引用次数变成0时将被调用的删除器。这启发我们创建一个null tr1::shared_ptr并以getRidOfInvestment作为删除器
1
std::tr1::shared_ptr<Investment>pInv(static_cast<Investment*>(0),getRidOfInvestment);
  1. tr1::shared_ptr有一个特别好的性质是,它会自动使用它的“每个指针专属的删除器”,因而消除另一个潜在的客户错误:所谓的“cross-DLL problem"。这个问题发生于对象在动态链接程序库DLL中被new创建,却在另一个DLL内被delete销毁,在许多平台上,这一类跨DLL之new/delete成对使用会导致运行期错误。
  2. 好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质。
  3. “促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容。
  4. “阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
  5. trl::shared_ptr支持定制型删除器(custom deleter)。这可防范DLL问题,可被用来自动解除互斥锁(mutexes;见条款14)等等。

19. 设计class犹如设计type

  1. 新type对象应该如何被创建和销毁?(第八章)
  2. 对象的初始化和对象的赋值该有什么样的差别?(条款四)
  3. 新type的对象如果被passed by value(以值传递),意味着什么? copy构造函数用来定义一个type的pass-by-value如何实现
  4. 什么是新type的”合法值“
  5. 你的新type需要配合某个继承图系吗?
    1. 如果继承自某些既有的类,就会受到那些类的设计的束缚,特别是他们的函数时virtual或non-virtual的影响(条款34和36)
    2. 如果允许其他类继承你的class,那会影响你所声明的函数,尤其是析构函数是否为virtual(条款7)
  6. 你的新type需要什么杨的转换
    1. 如果允许T1隐式转换为T2,就必须在class T1内写一个类型转换函数(operator T2)或者在class T2内写一个non-explicit-one-argument(可被单一实参调用)的构造函数,如果只允许explicit狗在函数存在,就得写专门负责转换的函数,且不得为类型转换操作符(type conversion operators)或者non-explicit-one-argument构造函数。(条款15)
  7. 什么样的操作符和函数对此新type而言时合理的?
    1. 决定于将为class声明哪些函数,其中某些该是member函数,某些则否(见条款23,24,46)
  8. 什么样的标准函数应该驳回?
    1. 那些正是你必须声明为private者(条款6)
  9. 谁该取用新type的成员
    1. 决定哪个成员为public,哪个为protected,哪个为private
    2. 决定哪一个类或者函数应该是friends,以及将它们嵌套于另一个之内是否合理
  10. 什么是新type的”未声明接口“
    1. 对效率、异常安全性(条款29)以及资源运用(例如多任务锁定和动态内存)提供何种保证,
  11. 你的新type有多么一般化
    1. 定义一个新的class template,定义一整个types家族
  12. 你真的需要一个新type吗
    1. 如果只是定义新的派生类以便为既有的class添加机能,说不定单纯定义一个或者多个non-member函数或者templates,更能达到目标

20. 宁以pass-by-reference-to-const替换pass-by-value

  1. 缺省情况下C++以by value方式传递对象至函数
  2. 除非另外指定,否则函数参数都是以实际参数的副本为初值,而调用端所获得的亦是函数返回值的一个副本,这些副本是对象的copy构造函数产出,使得pass-by-value成为费时的操作。
  3. 如何回避所有的构造和析构动作?pass by reference-to-const
1
bool validateStudent(const Student& s);
  1. 这种传递方式效率高,没有任何构造函数和析构函数被调用,因为没有任何新对象被创建。const是重要的,
  2. 以by reference方式传递参数可以避免slicing(对象切割)问题。当一个派生类对象以by value方式传递并被视为一个基类对象,基类的copy构造函数会被调用,而造成此对象的行为像个派生类对象的那些特化性质全被切割掉了,仅仅留下一个基类对象,但这几乎不是想要的
  3. references往往以指针实现出来,因此pass by reference意味真正传递的是指针。如果对象属于内置类型(int等),pass by value往往比pass by reference效率高。对于内置类型而言,有机会采用pass-by-value或pass-by-reference-to-const时,选择pass-by-value并非无道理。
  4. 同时也适用于STL迭代器和函数对象,因为它们习惯上被设计为passed by value
  5. 内置类型都相当小,对象小并不意味着其copy构造函数不昂贵。许多对象,包括大多数STL容器,内含的东西只比指针多一些,但复制这种对象却需要承担复制那些指针所指的每一样东西,那将非常昂贵
  6. 小型的用户自定义类型不必然成为pass by value优良候选人
  7. 尽量以pass-by-reference-to-const替换pass by value,前者通常高效,并可避免切割问题
  8. 以上规则并不适用内置类型,以及STL的迭代器和函数对象。对他们而言,pass-by-value往往比较适当。

21.必须返回对象时,别妄想返回其reference

  1. 可能会传递一些references指向其实并不存在的对象
  2. 函数创建新对象的途径:在stack和heap空间创建
  3. 不能返回reference指向的local 对象,
  4. 若在heap内构造一个对象,返回指向的reference,同样有问题,delete该如何调用?
  5. 一个必须返回新对象的函数的正确写法是,让那个函数返回一个新对象
1
2
3
4
inline const Rational operator* (const Rational &lhs, const Rational& rhs)
{
return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}
  1. 需要承受operator *返回值的构造和析构成本
  2. 当你必须在“返回一个reference和返回一个object”之间抉择时,你的工作时挑出行为正确的那个
  3. 绝不要返回pointer或reference指向一个local stack对象,或者返回reference指向一个heap-allocated对象或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。条款4已经为“单线程环境中合理返回reference指向一个local static对象”提供一个设计实例。

22. 将成员变量声明为private

  1. 如果成员变量不是public,客户唯一能够访问对象的办法就是通过成员函数。如果public接口内的每样东西都是函数,客户就不需要打算访问class成员时迷惑的试着记住是否该使用小括号
  2. 如果通过函数访问成员变量,日后可改以某个计算替换这个成员变量,而class客户一点也不会知道class内部实现已经起了变换
  3. 将成员变量隐藏在函数接口的背后,可以为“所有可能的实现”提供弹性。例如这可以使得成员变量被读或被写时轻松通知其对象、可以验证class的验证条件以及函数的前提和时候状态、可以在多线程环境中执行同步控制等
  4. 封装的重要性。对客户隐藏成员变量,可以确保class的约束条件总是会获得维护
  5. protected成员变量的论点类似。成员变量的封装性与“成员变量的内容改变时所破坏的代码数量”成反比。
  6. 假设有一个protected成员变量,而我们最终取消了它,所有使用它的派生类都会被破坏,同样缺少封装习惯。
  7. 从封装角度来说,只有两种访问权限:private(提供封装)和其他(不提供封装)
  8. 将成员变量声明为private。这可赋予客户访问数据的一致性,可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性
  9. protected并不比public更具封装性。

23. 宁以non-member、non-friend替换member函数

1
2
3
4
class WebBrowser{
public:
void clearCache();
void clearHistory();
void clearEverythinng();

};

//non-member函数 void clearBrowser(WebBrowser& wb) { wb.clearCache(); wb.clearHistory(); }

1
2
3
4
5
6
7
8
9
10
11
12
1. 面向对象守则要求,数据以及操作数据的那些函数应该捆绑在一起,member函数时较好的选择。这是对基于对面向对象真实意义的一个误解。面向对象守则要求数据应该尽可能被封装。
2. 如果member函数的封装性比non-member低。
3. 愈多东西被封装,我们改变那些东西的能力就越大,这就是推崇封装的原因,它能使改变事物而只影响有限客户
4. 能够访问private成员变量的函数只有class的member函数加上friend函数而已
5. 如果在一个member函数和一个non-member,non-friend函数之间选择,而且两者提供相同机能,那么导致封装性较大的时non-member non-friend函数,因为它并不增加“能够访问class内之private成分”的函数变量。
6. 让clearBrowser成为一个non-member函数并位于WebBrowser所在的同一个namespace

​```cpp
namespace WebBrowserStuff{
class WebBrowser{};
void clearBrowser(WebBrowser& wb);
}
  1. namespace和classes不同,前者可以跨越多个源码文件而后者不能。
  2. 将所有便利函数放在多个头文件内但隶属同一个命名空间,意味着客户可以轻松扩展这一组便利函数。要做的是添加更多non-member non-friend函数到此命名空间
  3. 宁可拿non-member non-friend函数替换member函数,这样做可以增加封装性、包裹弹性和机能扩充性

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

  1. 令classes支持隐式转换是个糟糕的主意,最常见的例外就是在建立数值类别时。假设设计一个class用来表现有理数,允许整数“隐式转换”合理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Rational{
public:
Rational(int num1 = 0, int num2 = 1);
int num1() const;
int num2() const;
//const Rational operator* (const Rational& rhs) const;
};
const Rational operator*(const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.num1() * rhs.num1(), lhs.num2() * rhs.num2());
}
Rational result;
result = R1 * 2;
result = 2 * R1;
  1. member函数的反面时non-member函数,而不是friend函数
  2. 如果需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member

25. 考虑写出一个不抛异常的swap函数

  1. swap函数原来只是STL的一部分,而后成为异常安全性编程的脊柱,以及用来处理自我复制的可能性的一个常见机制。还有非凡的复杂度。
1
2
3
4
5
6
7
8
9
namespace std{
template<typename T>
void swap(T& a, T& b)
{
T temp(a);
a = b;
b = temp;
}
}
  1. 主要类型T支持copying,缺省的swap实现代码会帮你置换类型T的对象
  2. 声明一个non-member函数调用member函数,
  3. 有些类型,swap的缺省行为会降低速度,如以指针指向一个对象,内含真正数据的类型,这种类型的常见表现形式是所谓的“pimpl手法”。
  4. 只要置换其Pimpl指针,将std::swap针对Widget特化
1
2
3
4
5
6
7
namespace std{
template<>
void swap<Widget>(Widget& a, Widget& b)
{
swap(a.pImpl, b.pImpl);
}
}
  1. template<>表示它是std::swap的一个全特化版本,函数名称之后的表示这一特化版本系针对“T是Widget”而设计的。一般性的swap template施行于Widgets身上便会启用这个版本。通常不能够改变std命名空间内的任何东西,但可以为标准templates制造特化版本,使它专属于我们自己的classes
  2. 令Widget声明一个名为swap的public成员函数做真正的置换工作,然后将std::swap特化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Widget{
public:
void swap(Widget& other)
{
using std::swap;
swap(pImpl, other.pImpl);
}
};

namespace std{
template<>
void swap<Widget>(Widget& a, Widget& b)
{
a.swap(b);
}
}
  1. 这种做法不只能够通过编译,还与STL容器有一致性,因为所有STL容器都是有提供public swap成员函数和std::swap特化版本(用以调用前者)
  2. C++只允许对class template偏特化(partially specialize),在function template偏特化行不通
  3. 当打算偏特化一个function template时,惯常做法是简单为它添加一个重载版本,声明一个non-member swap让它调用member swap,但不再将那个non-member swap声明为std::swap的特化版本或者重载版本。
1
2
3
4
5
6
7
8
9
10
namesapce WidgetStuff{
template<typename T>
class Widget{};

template<typename T>
void swap(Widget<T>& a, Widget<T>& b)
{
a.swap(b);
}
}
  1. C++的名称查找法则确保将找到global作用域T所在之命名空间内的任何T专属的swap。如果T是Widget并位于命名空间WidgetStuff内,编译器会使用“实参取决之查找规则”找出WidgetStuff内的swap。如果没有T专属之swap存在,编译器就使用std内的swap,这得感谢using表达式让std::swap在函数内曝光。即便如此编译器还是比较喜欢std::swap的T专属特化版,而非一般化的那个template,如果已针对T将std::swap特化,特化版会被编译器挑中。
  2. 首先,如果swap的缺省实现码对你的class或class template提供可接受的效率,你不需要额外做任何事。任何尝试置换那种对象的人都会取得缺省版本,而那将有良好的运作。
  3. 其次,如果swap缺省实现版的效率不足(那几乎总是意味你的class或template使用了某种pimpl手法),试着做以下事情:
    1. 提供一个public swap成员函数,让他高效的置换你的类型的两个对象值,这个函数绝不该抛出异常
    2. 在你的class或template所在的命名空间内提供一个non-member swap,并令它调用上述swap成员函数
    3. 如果你正在编写一个class(而非class template),为你的class特化std::swap。并令它调用你的swap成员函数
  4. 最后,如果你调用swap,请确定包含一个using声明式,以便让std::swap在你的函数内曝光可见,然后不加任何namespace修饰符,赤裸裸地调用swap
  5. 当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。
  6. 如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于classes,(而非templates),也请特化std::swap
  7. 调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何“命名空间资格修饰”
  8. 为“用户定义类型”进行std templates全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西

4. 设计与声明
http://binbo-zappy.github.io/2024/11/27/effective-cpp/4-设计与声明/
作者
Binbo
发布于
2024年11月27日
许可协议