LucienXian's Blog


  • 首页

  • 归档

  • 标签

Effective-cpp-#33

发表于 2018-02-13

Avoiding hiding iherited names

这里的关键是区分清楚重载与重写

继承一个基类时,如果其下的public函数不带virtual,而派生类又重写了一个同名的函数,那么派生类就会把基类版本的该函数给掩盖住了,也就是无法再通过派生类去调用基类版本的函数了

解决方法

有两种解决方法:

第一种是使用using声明式:

1
2
3
4
5
6
7
8
9
10
11
class Base{};

class Derived: public Base{
public:
//保证Base class内名为mf1和mf3的所有东西在Derived域内可见
using Base::mf1;
using Base::mf3;
virtual void mf1();
void mf3();
void mf4();
};

通过这种手段,继承机制没有问题,在派生类找不到的函数(版本),会继续到基类作用域内寻找。

第二个方法就是使用forwarding function,特别是你如果想要隐藏到基类不同参数的函数中某一个,用例子说话:

1
2
3
4
5
6
7
8
9
10
11
12
class Base{
public:
virtual void mf1() = 0;
virtual void mf1(int);
};

class Derived: private Base{
public:
virtual void mf1(){
Base::mf1();
}
};

这样就隐藏掉带参数的mf1()了

建议

  • Derived class内的名称会隐藏掉base class内的函数。在public继承体系下,我们不希望发生这样的事情;
  • 为了重新获取被掩盖的基类函数,可以使用using声明式或者forwarding functions

Effective-cpp-#32

发表于 2018-02-12

Make sure public inheritance models "is-a"

通常来说,基类是比派生类更加一般化的概念,所以任何希望获得一个基类的函数,都可以接受一个派生类对象作为参数。

通常来说,继承体系都有一个比较明显模型:is-a。但这里经常遇到的问题是,举个例子:

1
2
3
4
5
6
7
8
class Bird {
public:
virtual void fly();
}

class Penguin: public Bird{
...
}

在这个例子中,问题出现在——企鹅是鸟,但企鹅不可以飞。也就是构造了一个不严谨的继承体系。当然,这跟你程序设计有关,有可能你设计时不希望有一个Bird::fly()的函数,那样就不会出现问题。

通常来说,我们希望的是:“public继承”则意味着is-a,也就是说,适用于base class的每一个行为也应该适用于derived class。

Effective-cpp-#31

发表于 2018-02-11

Minimize compilation dependencies between files

动机

这个问题主要是在多文件的程序中,如果某个class实现文件做了些许的修改,当你重新build/make这个项目时,可能所有相关依赖的文件都重新编译和链接了。这个就是“接口和实现没有很好分离的一大弊端”。

考虑这样一个程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
//可能包含的头文件
#include <string>
#include "date.h"
#include "address.h"

class Person{
public:
...
private:
std::string theName;
Date theBirthDate;
Address theAddress;
}

这里的问题是:在private中的那几个变量定义,我们需要找到Date和Address的具体定义,也就是说一旦这个文件依赖的头文件发生了变化,那么Person class也要重新编译。

解决方法

当然,我们可以分离声明和定义,比如在刚刚那个文件中声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Date;
class Address;

class Person{
public:
...
private:
std::string theName;
Date theBirthDate;
Address theAddress;
}

//另外的文件
int main()
{
int x;
Person p(params);
}

但这里的问题是,对于前置声明,编译器必须提前知道对象的大小,这样才能在stack中分配足够大的空间。

借鉴Java的经验,我们用一个指针去指向需要的对象。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <string>//一般来说,标准库组件不用前置声明
#include <memory>

class PersonImpl; //Person实现类的前置声明
class Date;
class Address;

class Person{
public:
...
private:
std::trl::shared_ptr<PersonImpl> pImpl;
}

这种设计就叫做——pointer to implementation。至此,Person的客户端就与其他class包括Person的实现细节分离了,也就是其他类的修改不会影响到Person客户端重新编译。

制作一个handle class

通常来说,我们应该为声明式和定义式提供不同的头文件,这样就不用每次都前置声明其他的class。

制作一个handle class的方法有两种,第一种已经在前面提及过了,至于第二种就是构造一个抽象类:

1
2
3
4
5
6
7
8
9
10
11
12
class Person{
public:
virtual ~Person();
virtual std::string name() const = 0;
virtual std::string birthDate() const = 0;
virtual std::string address() const = 0;
...
static std::trl::shared_ptr<Person>
create(const std::string& name,
const Date& birthday,
const Address& addr);
}

当然,由于这是abstract class,我们需要一个concrete class来定义所有的函数。

建议

  • 为了保证编译依赖最小化,我们应该依赖于声明而不是定义。这里有两个实现手段——handle class和interface class;
  • 程序库头文件应该以“完整的并且是仅有声明”的形式存在;

Effective-cpp-#30

发表于 2018-02-11

Understand the ins and outs of inlining

inline函数,不是第一次接触这个概念了。编译器的最优化机制会被设计用来执行语境相关的最优化。但有利有弊,由于inline函数的主体会被插到调用处。这样一来,你的目标码会变得越来越大,有可能导致额外的paging行为。


通常来说,inline函数有两种定义方法——一是在类内定义函数,这是一种隐喻的方式;二是在定义式前加inline关键字。

注意

inline函数尽量置于头文件内,因为大多数build environment通常会在编译时进行inlining,这是因为为了将一个“函数调用”替换为“被调用函数的本体”,编译器必须知道那个函数长什么样子。

template的具体化与inline无关,我们需要的考虑的是——是否希望每一个具体实现的函数都是inline的,毕竟这有可能会引发代码膨胀。

关于编译器的问题,编译器不一定就真的会对inline函数进行inline行为,例如编译器通常不会对“通过函数指针而进行的函数调用”进行inline行为。另外,如果程序要调用某个inline函数,往往,编译器会生成一个outlined函数本体

最后一个需要注意的问题是,inline函数无法随着程序库的升级而升级。假设f是一个inline函数,而设计者修改了这个函数f,那么所有用到f的客户端程序都必须重新编译,而对于non-inline函数,客户端只需要重新链接即可,这比重新变异的负担少。

建议

  • 将大多数inline限制在小型的、被频繁调用的函数身上。这可以使得日后的调试和二进制升级更加容易,也可以使得潜在的代码膨胀问题最小化;
  • 不要只因为function template出现在头文件,就将它们声明为inline;

Effective-cpp-#29

发表于 2018-02-11

Strive for exception-safe code

动机

我们常说的异常安全需要满足两个条件:

  • 不泄露任何资源
  • 不允许数据被破坏

考虑这样一段代码:

1
2
3
4
5
6
7
8
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
lock(&mutex);
delete bgImage;
++imageChanges;
bgImage = new Image(imgSrc);
unlock(&mutex);
}

这段代码的问题就是,一旦“new Image(imgSrc)”出现了异常,那么unlock就不能执行了,互斥器就永远锁住了,则泄露了资源;另外就是这样bgImage会指向一个被删除的对象,而imageChanges被破坏了

解决方法

解决方法其实很简单,则使用之前提及过的条例:以对象去接管资源:

1
2
3
4
5
6
7
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
Lock m1(&mutex);
delete bgImage;
++imageChanges;
bgImage = new Image(imgSrc);
}

一般来说,一个异常安全函数(Exception-safe function)必须提供以下的三个保证之一:

  • 基本承诺:如果异常被抛出了,那么程序的成员应该要保持有效的状态,则没有数据被破坏了。以上面的函数为例,我们可以修改程序,在函数抛出异常时,bgImage指向一个默认的对象;

  • 强烈保证:保证程序只有两个状态:异常抛出,状态改变;否则就回到程序调用函数之前的状态;

  • nothrow保证:承诺永远不会抛出异常;


另外一个保证异常安全的方法是——copy&swap,则为你希望修改的对象构造一个副本,然后在那个副本上做一切修改,若有任何修改操作发生异常,那么原对象不受影响。待修改成功后,在交换指针。

建议

  • 异常安全函数即使发生异常也不会泄露资源或者允许任何数据结构被破坏;
  • 强烈保证可以通过copy&swap实现出来;
  • 函数提供的“异常安全保证”通常最高只等于所调用的各个函数“异常安全保证”的最弱者;

Effective-cpp-#28

发表于 2018-02-11

Avoid returning "handles" to object internals

动机

先来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Point{...};

struct RectData{
Point ulhc;
Point lrhc;
};

class Rectangle{
private:
std::trl::shared_ptr<RectData> pData;
public:
Point& upperLeft() const{return pData->ulhc;}
Point& lowerRight() const {return pData->lhrc;}
};

使用:

1
2
const Rectangle rec(point1, point2);
rec.upperLeft().setX(50);

其实,这样使用编译器是不会报错的,但其出现了矛盾的情况,原因是我们在该函数后面指定了const,则希望rec是不变的。但我们可以看到,由于我们返回的是一个引用,则可以通过引用去修改对象内部的成员。
同理,返回指针或者迭代器这些handles,也会出现这种情况。

解决方法

为了解决这个问题,我们可以这样:

1
2
3
4
5
class Rectangle{
...
const Point& upperLeft() const{return pData->ulhc;}
const Point& lowerRight() const {return pData->lhrc;}
};
通过这种做法,用户可以读取对象的points,但不能修改它们。

但这种用法同样会有一些问题,那就是有可能会出现dangling handles。

1
2
3
4
5
6
class GUIObj{...};
const Rectangle boudingBox(const GUIObj& obj);

GUIObj* obj;
...
const Point* p = &(boudingBox(*p).upperLeft());
这里的问题是,由于这里够早的是一个临时Rectangle对象,也就是说,当这个语句结束时候临时对象就会被销毁,那么p所指向的Point也会被析构。也就是会发生dangling handles。

这是因为返回一个handle代表对象内部成员的话,不管这个handle是指针还是引用,也不管是不是const,这里的关键是一旦这个handle传出去了,你就得承受一个风险——handle可能会比其所指向的对象有着更长的声明周期。

建议

  • 避免返回任何handles指向对象的内部。另外,还要使得const成员函数的行为像个const,并且尽量避免发生dangling handles。

Effective-cpp-#27

发表于 2018-02-09

Minimize casting

动机

C++的设计目标就是要保证“类型错误”不可能发生,使得程序能够“干净地”通过编译。

使用

一般来说,在C++中有两种转型——"old-style casts"和"new-style casts"。 * old:(T)expression或者T(expression); * new:

1
2
3
4
const_cast<T>(exp)
dynamic_cast<T>(exp)
reinterpret_cast<T>(exp)
static_cast<T>)exp

  1. const_cast:通常用来去除对象的常量性;
  2. dynamic_cast:主要执行“安全向下转型”,但会耗费大量的运行成本;
  3. reinterpret_cast:主要是执行低级转型,实际结果取决于编译器,所以其不可移植。例如将一个pointer to int转为int;
  4. static_cast:用来强迫隐式转换,例如将non-const转为const,int转为double,同样也可以是void*转为typed指针,或者pointer to base转为pointer to derived


之所以要用新式转换,有两个原因:一是很容易能够在代码中辨认出来,便于debug;二是将转型动作窄化,编译器更可能诊断出错误运用。
至于什么时候用旧式转换——调用一个explicit构造函数讲一个对象传递给一个函数时。例如:

1
2
3
4
5
6
7
class Widget{
public:
explicit Widget(int size);
}
void doSomeWork(const Widget& w);
doSomeWork(Widget(15));
doSomeWork(Static_cast<Widget>(15));

往往,转型动作会令到编译器编译出运行期间不同执行的码。例如:

1
2
3
4
class Base{...};
class Derived: public Base{...};
Derived d;
Base* pd = &d;
我们可以看到,在这种情况下建立一个base class指针指向一个derived class对象,会有一个偏移量在运行期被施行于Derived指针身上,用以取得正确的Base 指针值.

另一个有趣的事情是,如果我们希望在derived classes内的virtual函数代码第一个动作先调用base class的对应函数。使用以下的方式,其实是错误的:
1
2
3
4
5
6
7
8
9
10
11
12
13
class Window{
public:
virtual void onResize(){...}
...
};

class SpecialWindow: public Windos{
public:
virtual void onResize(){
static_cast<Window>(*this).onResize();
}
...
}
其实这种行为,调用的并不是当前对象上的函数,而是稍早转型动作所建立的一个"*this对象的base class成分"的暂时副本身上的onResize!


再来看看dynamic_cast,之所以需要dynamic_cast,通常是因为你只有一个“指向Base”的指针或引用,但希望在一个你认定为derived class对象身上执行derived class操作函数。
对于这种情况,有两个解决办法:
使用容器并在其中存储指向derived class对象的指针,并且尽量避免转型:
1
2
3
4
5
6
7
typedef std::vector<std::trl::shared_ptr<SpecialWindow> >VPSW;
VPSW winPtrs;
...
for (VPSW::iterator iter = winPtrs.begin();
iter != winPtrs.end();
++iter)
(*iter)->blink();
但是这种方法,不能在同一个容器中存储指针“指向所有可能的Window派生类”。

另外一种做法就可以让你通过base class 接口处理“所有可能的各种Window派生类”,那就是在base class内提供virtual函数做你想对各个Window派生类做的事。当然,在base class只是提供一个“什么都不做”的缺省实现码。

建议

  • 如果可以,尽量避免转型,尤其是注重效率的代码中避免使用dynamic_cast;
  • 如果转型是必要的,试着将其隐藏在某个函数背后。客户随后直接调用该函数,而不需要将转型放进自己的代码内;
  • 宁愿使用新式转型,不要使用旧式转型;

Effective-cpp-#26

发表于 2018-02-09

Postpone variable definitions as long as possible

动机

这个的条例提出的原因是——当你定义了一个带有构造函数或者析构函数的变量时,那么当程序的control flow到达这个变量的定义表达式时,你就要承受其构造的成本,同样,当离开变量作用域时,还需要承受析构成本。

关键是,即便你没有使用这个变量,也要耗费这些成本

考虑一个函数:

1
2
3
4
5
6
7
8
9
10
std::string encryptPassword(const std::string& password)
{
using namespace std;
string encrypted;
if (password.length()<MinimumPasswordLength)
throw logic_error("Password is too short");
...
return encrypted;
}

这个函数的命题是,如果有异常被抛出,那么对象encrypted就没被使用,但你仍然需要承受其构造和析构的成本;

解决方法

解决方法很简单,什么时候使用就什么时候定义。但这个过程需要注意的是通常情况下我们应该尽量避免使用default构造函数,应该在构造时就赋予初值。

另外有一个需要注意的问题是,循环怎么办?

1
2
3
4
5
6
7
8
9
10
11
//Method A
Widget w;
for (int i=0; i<n; i++){
w = 取决于i的值;
}

//Method B

for (int i=0; i<n; i++){
Widget w = 取决于i的值;
}
需要衡量的是: * 方法A:1个构造函数+1个析构函数+n个赋值操作 * 方法B:n个构造函数+n个析构函数

通常情况下,当n比较大时,方法A比较好;否则应该选B

建议

  • 尽量延后变量定义式的出现。这样做可以增加程序的清晰度并改善程序效率;

Effective-cpp-#24

发表于 2018-02-06

Declare non-member functions when type conversions should apply to all parameters

动机

通常来说,让一个类支持隐式是一个糟糕的设计,它会使得程序变得复杂。但在某些时候,我们需要隐式的转换,考虑一个有理数的类:

1
2
3
4
5
class Rational{
public:
Rational(int numerator=0, int denominator=1);
...
};


如果希望支持算术运算符诸如加法,乘法,那么可以这样实现:

1
2
3
4
5
class Rational{
public:
...
const Rational operator* (const Rational& rhs) const;
};

但这种设计有个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
Rational oneEighth(1, 8);
Rational oneHalf(1, 2);
Rational result = oneHalf * oneEighth; //可以通过

result = oneHalf * 2; //也可以通过,等同于
result = oneHalf.operator*(2);
//通过的原因,编译器构造了临时对象
const Rational temp(2);
result = oneHalf*temp;

//而如果是想要满足乘法交换律,则会失败:
result = 2 * oneHalf;
//因为2没有相应的class,也就没有operator*函数。当然编译器会尝试在命名空间内或者在global作用域内调用non-member operator*,而这也找不到

解决方法

当然,可以声明explicit 构造函数,这也就保证了一致性,但就无法满足了乘法运算。

为了解决这个问题,也满足了四则运算,我们可以构造一个non-member函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Rational{
...
};

//构造一个non-member函数
const Rational operator*(const Rational& lhs,
const Rational& rhs)
{
return Rational(lhs.numerator()*rhs.numerator(),
lhs.denominator()*lhs.denominator());
}

//这样,就都可以通过了
Rational result;
result = oneFourth * 2;
result = 2 * oneFourth;

这样,就基本解决问题了。。。。

建议

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

Effective-cpp-#23

发表于 2018-02-05

Prefer non-member non-friend functions to member function

动机

考虑两种程序组织方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
class WebBrowser {
public:
...
void clearCache();
void clearHistory();
void removeCookies();
...
void clearEverything(){
this.clearCache();
this.clearHistory();
this.removeCookies();
}
};
用一个非成员函数去封装
1
2
3
4
5
void clearBrowser(WebBrowser& wb){
wb.clearCache();
wb.clearHistory();
wb.removeCookies();
}

到底选择哪一种好呢?我们先从封装开始考虑起,越多东西被封装,就意味着越少人可以看到它,那么就会有更大的弹性去改变它。 通常来说,越多函数可以访问它,就意味着数据的封装性越低。



解决方法

因此通常来说,如果有两个程序组织方式————member函数和non-member函数两种,而它们的机能相同,那我们应该选择封装性更大的non-member函数,因为它们不会增加"能够访问"class内的private成分的函数数量


另外,尽管我们将使用non-member函数,但这并不意味着该函数不能成为其它类的一部分,例如,我们可以另void clearBrowser()其成为某些工具类的static member函数

通常来说,在C++中比较自然的做法是将non-member函数与类放在同一个namespace中:

1
2
3
4
namespace WebBrowserStuff{
class WebBrowser {...};
void clearBrowser(WebBroser& wb);
}

建议

  • 宁可拿non-member non-friend 函数来替换member函数。这样做可以增加封装性,package flexibility和机能扩充性。
<i class="fa fa-angle-left"></i>1…212223…28<i class="fa fa-angle-right"></i>

278 日志
29 标签
RSS
© 2025 LucienXian
由 Hexo 强力驱动
主题 - NexT.Pisces