Xander's Wiki


  • Home

  • Tags

  • Categories

  • Archives

  • Search

8.为多态基类生成virtual析构函数

Posted on 2018-04-08 | In Effective C++

问题描述

假设我们定义了一个base class,并随之延伸出一些derived class。用户只需要在程序中使用某个功能,并不关心其实现。因此,我们可以设计一个factory函数。(工厂模式详见More Effective C++ 25)
factory函数返回一个base clsss指针,指向新生成的derived class对象。 返回的对象必须位于heap,因此为了避免泄漏内存和其他资源,factory返回的每一个对象都应该被适当的delete。举例而言:

1
2
3
4
5
6
7
8
9
10
11
12
class TimeKeeper{//计时器基类
public:
TimeKeeper();
~TimeKeeper();//此处不应该使用non-virtual析构函数
...
}
class AtomicClock:public TimeKeeper{...};
class WaterClock:public TimeKeeper{...};

TimeKeeper* ptk=getTimeKeeper();//factory函数
...
delete ptk;释放以避免资源泄露

问题在于factory返回的指针指向一个derived class对象,而那个对象却经由一个base class指针被删除。如果base class的析构函数是non-virtual,则我们再次雪崩。


多态基类不使用virtual析构的后果

c++明确指出,当derived class对象由一个base class指针被删除,而base class带着一个non-virtual析构函数,其结果未定义 — 一般来说,实际执行时对象的derived成分没被销毁,derived class的析构函数也没执行。也就是说我们得到了一个诡异的局部销毁对象。


何时需要virtual析构函数?

一般来说,base class都会有那么一个virtual function。我们在这里指出,只要一个class有virtual function,我们都应该把它的析构函数定义为virtual。

如果一个class不被用作base class,那给他定义一个virtual 析构函数非常愚蠢。原因是这样的:
欲实现出virtual函数,对象必须携带某些信息,要来指出在运行期哪个virtual函数被调用。这份信息一般由一个virtual table pointer指出。vtp指向一个vbtl(virtual table)每一个带virtual函数的class都会有一个vtbl,当函数调用某个virtual函数,实际被调用的函数取决于对象的vptr所指向的vtbl—编译器在其中寻找函数指针。(关于虚函数的使用成本,详见More Effective C++ 24)
总之,虚函数的引入增加了class的大小,并且破坏了原有的内存结构。

另外,STL所有的类都不能作为base class,它们也没有virtual析构函数。


virtual析构函数在abstract class中的用法

有时我们希望定义一个abstract class,但是手头上并没有pure virtual function,那解决方法很简单:声明一个pure virtual 析构函数。
但事情还没完,我们不仅仅需要声明,还需要定义这个析构函数。可能你会诧异于pure function居然还需要实现,但实际上pure function和有没有实现并没有任何联系。(该论述详见More Effective C++ 33),具体声明和定义如下所示:

1
2
3
4
5
class c{
public:
virtual ~c() = 0;//声明式
};
c::~c() {}//定义式

因为析构函数的运作规则是是从最深层派生的class开始调用其析构,然后是每一个base class。在derived class的析构动作中编译器会创建一个对其基类析构函数的调用动作,如果没有定义,连接器会报错。


是否只要是base class就需要析构函数?

正如标题所说,只有使用了多态性的base class(具备虚函数)才需要virtual析构。
诸如上节提到的uncopyable类,其根本不需要使用virtual 析构函数。


总结

  1. polymorphic base class应该声明一个virtual析构。如果一个类至少有一个virtual function,把它也应该拥有一个virtual析构函数
  2. class 不具有多态性或者根本不作为base class,那就不要搞virtual析构。

8.切勿创建包含auto_ptr的容器对象

Posted on 2018-04-08 | In Effective STL

Contain of auto_ptr (COAP)被禁止使用,其代码无法编译。原因很简单,COAP无法移植,再究其根本,在于auto_ptr被复制后,它所指向的对象的所有权被移交给lhs,而自身被置为nullptr,其特性可以用代码表述如下:

1
2
3
auto_ptr<Widget> pw1(new Widget); // pw1指向一个Widget
auto_ptr<Widget> pw2(pw1); // pw2指向pw1的Widget,pw1被设为nullptr
pw1 = pw2; // pw1现在再次指向Widget,pw2被设为nullptr

举一个COAP的例子,它建立一个包含auto_ptr<Widget>的vector,然后对这个vector内部元素排序,其排序指标是一个谓词:
1
2
3
4
5
bool widgetAPCompare(const auto_ptr<Widget>& lhs,const auto_ptr<Widget>& rhs) {
return *lhs < *rhs;
}
vector<auto_ptr<Widget> > widgets;
sort(widgets.begin(), widgets.end(),widgetAPCompare);//无法编译

这段代码看起来很合理,但实际上不行,原因在于在排序过程中widget中的一个或多个auto_ptr被置为null.之所以会这样,是快排的锅。快排的基本思想在于把容器内的某个元素作为pivot elements,然后对大于和小于等于该元素的其他元素递归调用排序。其方法看起来像这样:
1
2
3
4
5
6
7
8
template<class RandomAccessIterator,class Compare> 
void sort(RandomAccessIterator first,RandomAccessIterator last,Compare comp){
typedef typename iterator_traits<RandomAccessIterator>::value_type ElementType;
RandomAccessIterator i;
... // 让i指向主元
ElementType pivotValue(*i);//将主元拷贝到一个局部变量中
...//接着排序
}

这个typename挺有讲究,强调了后面是一个类型,但我们不管,重点在于那个拷贝,它把auto_ptr的值放到了一个局部临时中,随着临时对象生命周期结束,对象也被析构了,真是倒了血霉。

所以说,不要搞包含auto_ptr的容器,其他智能指针配合容器倒是可以用用。

7.明确拒绝编译器自动生成的函数

Posted on 2018-04-07 | In Effective C++

某些类可能不允许被拷贝,我们可能会试图去不声明copy构造函数和copy assignment操作符来让用户无法copy,但这毫无意义,因为编译器还是会生成它们。

解决的关键在于编译器所自动生成的这些函数都是public的,如果我们声明一个private的copy构造函数以及copy assignment运算符,并且不去定义它们(以防止友元访问),那么这个类自然就无法copy了。

当你把它们声明为private时,如果用户试图去拷贝,此时编译器会报错,如果我们通过友元或者在成员函数内试图调用它们,因为没有定义,此时轮到连接器发出警告。

当然,越早报错越好,那能不能把连接期错误移至编译期呢?答案是可行的。
为了实现上述理想,我们需要借助一个base class,它什么也不干,就是为了阻止copy。

1
2
3
4
5
6
7
8
class Uncopyable{
protected:
Uncopyable() {}
~Uncopyable(){}
private:
Uncopyable(const Uncopyable&);
Uncopyable& operator=(const Uncopyable)
}

如果我们需要令某个类无法被copy,那我们只需要让该类私有继承Uncopyable即可(关于采用何种方式继承最为恰当,详见Effective C++ 38 39)
当友元或者成员函数试图拷贝时,编译器会试着生成copy构造函数或者copy assignment操作符,这两个函数又会去调用base class中的版本,发现调用失败,编译报错。

Uncopyable的设计颇为微妙,值得仔细揣摩,比如说继承方式,析构函数,以及空类优化等诸多方面。但在本章,我们只关心它的作用:阻止拷贝。

总结

为了拒绝编译器生成的某些函数,我们可以将其设为private且不予实现。或者令该类继承类似于Uncopyable这样的基类。

7.在析构前记得delete容器内通过new得到的指针

Posted on 2018-04-07 | In Effective STL

由于容器在析构时会自发地销毁所有容器内部的元素,因此有人会忽视容器内部元素的清除工作。大部分时候确实不用手动地清除,仅有一个例外:容器内存放的是通过new得到的指针。容器析构时指针当然被销毁了,但容器绝不会自动地调用delete释放每一个指针指向的对象,而且我们再也找不到这些对象了。

1
2
3
4
5
6
void doSomething(){
vector<Widget*> vwp;
for (int i = 0; i < SOME_MAGIC_NUMBER; ++i)
vwp.push_back(new Widget);
//vwp析构后资源也泄露了
}

delete这些指针并不麻烦,
1
2
3
4
5
void doSomething(){
vector<Widget*> vwp;
for (auto i = vwp.begin();i != vwp.end();++i)
delete *i;
}

上述代码看起来似乎很美好,但是仍然存在两点不存:

  1. 做了for_each做的事,但是不如for_each直观
  2. 不具备异常安全,如果在向vwp填充指针或者从中删除指针时有异常抛出,同样会内存泄漏。

首先解决第一个问题,将上述代码改写为for_each:

1
for_each(vwp.begin(),vwp.end(),[](Widget* pw){delete pw;});

(原书中描述了大量关于函数对象及其使用原理的说明,在C++11引入lambda之后,笔者跳过了这些知识)

使用shared_ptr可以避免异常安全的问题,我们通过将原容器替换为包含智能指针的容器之后,一切都变得美好了起来。

6.了解C++自动生成与调用的函数

Posted on 2018-04-07 | In Effective C++

前言

即使我们写下

1
class Empty{};

这样的空类,它本质上也不是空的,而是类似于:
1
2
3
4
5
6
7
class Empty{
public:
Empty(){}
Empty(const Empty& rhs) {}
~Empty() {}
Empty& operator=(const Empty& rhs) {}
}

在我们没有写的情况下,编译器会自动地生成​default构造函数,copy构造函数,copy assignment操作符和析构函数。
这些函数都是inline并且public的。只有在他们被调用时,编译器才会创建它们。


自动生成的函数的性质与作用

default构造与default析构

default构造函数和析构主要是给编译器调用base clssses和non-static成员变量的构造函数和析构函数。这里需要注意的是,编译器生成的析构函数为non-virtual,除非this class 的base class自身声明有virtual析构函数。(此时thisclass的析构函数的virtualness源自base class)

copy构造函数与copy assignment操作符

至于copy构造函数与assignment操作符,编译器只是单纯地将来自源对象的non-static成员变量copy到目标对象。假设现有一个Widget Template,它允许你将一个string与一个T类型的数据发生关联:

1
2
3
4
5
6
7
8
9
10
template<typename T>
class Widget{
public:
Widget(const char* name,const T& name);
Widget(const string& name,const T& name);
...
private:
string thename;
T value;
}

因为已经声明了构造函数,所以编译器不会自动生成default构造函数,也就是说,一旦你创建了一个class,其对象需要一个参数才能构建,那你无需担心编译器会自动帮它生成无参构造函数。(关于无参构造函数的弊端可见More Effective 4)

Wideget既没有声明copy构造函数,也没有声明copy assignment操作符。因此编译器会自动生成它们,那么具体是如何实现的?

1
2
Widget<int> a("sth",5);
Widget b(a);//调用自动生成的copy构造函数

copy构造函数

编译器生成的copy构造函数会以a.thename与a.value为初值构造b.thename与b.value,因为string存在copy构造函数,因此它调用了string的copy构造函数生成了b.thename,int属于内置类型,不存在copy构造函数,因此b.value将会copya.value的每一个bits来完成初始化。

copy assignment操作符

copy assignment操作符和copy构造函数略有不同,原因在于并非任何成员对象都可以被赋值(const成员变量,reference)。
在c++中,不允许reference改指向不同对象,也不允许const对象被赋值。因此,此时copy assignment无法完成,编译器拒绝生成此操作。也就是说,当class内含const成员与reference成员时,C++不会生成copy assignment操作符。
如果我们试图令内含reference成员或const成员的class支持copy assignment,那我们必须要自定义copy assignment操作符。
还有一种情况C++也不会生成copy assignment操作符,就是base class将copy assignment定义为private.原因很简单,如果编译器为derived class生成了copy assignment操作符,那它必定会试图调用base class的copy assignment操作符来操作base class的数据成员,可惜它没有这个权限来调用。

总结

编译器会自动生成某些函数,谨记它们的性质、作用、以及在什么情况下不会自动生成。

6.注意C++的分析机制

Posted on 2018-04-07 | In Effective STL

假设我们有一个存有整数的文件,并试图把它们复制到一个list中,我们也许会写下

1
2
fstream dataFile("ints.dat");
list<int> data(istream_iterator<int>(dataFile), istream_iterator<int>());

这里的想法是传一对istream_iterator给list的区间构造函数,尽管它不会发生编译错误(还不如发生),但运行效果和期望的相差很大。准确的说,它并没有声明一个list,也没有构造。

我们先从简单的部分开始解释,在c++中,以下三种声明均是合法且等价的:

1
2
3
int f(double d);
int f(double (d));
int f(double);

接着看三个函数声明:
1
2
3
4
5
6
//g是一个返回值为int的函数,其形参是一个函数指针,该指针指向一个不需要形参且返回值为double的函数
int g(double (*pf)());
//同上,pf其实是一个函数指针
int g(double pf());
//同上,省略了参数名
int g(double ());

请注意围绕参数名的括号,比如第一组第二个的(d)与独立的括号的区别。围绕参数名的括号可以被忽略,而独立的括号则表明参数列表的存在,它说明前面存在一个函数指针参数。

返回到问题,我们仔细观察这一行语句:

1
list<int> data(istream_iterator<int>(dataFile), istream_iterator<int>());

这其实是声明了一个函数,其返回值是list<int>,函数名为data,接受两个参数:

第一个参数名为dataFile,是一个迭代器。(认为dataFile两侧括号多余)
第二个参数没有名称,它的类型是指向一个没有参数而且返回istream_iterator的函数的指针

顺便一提,

1
2
class Widget {...}; // 假设Widget有默认构造函数
Widget w();

这种东西自然是不能构造对象的,因为它其实是声明了一个不接受参数且返回一个Widget的函数。

为了解决我们遇到的这种烦人的分析机制,我们可以给迭代器名字:

1
2
3
4
ifstream dataFile("ints.dat");
istream_iterator<int> dataBegin(dataFile);
istream_iterator<int> dataEnd;
list<int> data(dataBegin, dataEnd);

尽管这种命名式实参的风格与STL相违背,但是为了避免二义性并增加可读性,这不失为一种优雅的解决方式。

5.确定对象被使用前已经初始化

Posted on 2018-04-07 | In Effective C++

何时需要初始化?

一般来说,使用c part of c++中的组件时,声明变量后不初始化就加以使用会得到未知结果,但non-c parts of C++的规则则略有变化。这也就是array不保证其内容被初始化,但vector却有保证的原因。
为了避免得到一些无谓的结果,我们的最佳处理方法就是永远在使用对象前对它进行初始化。


初始化方式

对于内置类型,我们以手工的形式完成初始化。但对于内置类型以外的其他任何东西,初始化交由构造函数来完成。我们的规则也十分简单:确保构造函数会初始化对象的每一个成员变量。关键在于不要混淆赋值和初始化的概念,从而导致效率降低。

自定义类型的赋值与初始化

假定存在class Person,其定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
class Person{
public:
Person(const string &name,const string &phoneNumber);
private:
string thename;
string thephoneNumber;
}
//构造函数
Person::Person(const string &name,const string &phoneNumber){
thename = name;//此处并非初始化而是赋值
thephoneNumber = phoneNumber;
}

c++规定,成员变量的初始化动作发生在进入构造函数本体之前。也就是说,刚才thename等变量已被default构造函数初始化过了。但️内置类型除外,因为它不具备default初始化。
因此构造函数的最佳写法是使用member initialization list替换赋值操作,这样会比赋值高效很多。例如:
1
2
Person::Person(const string &name,const string &phoneNumber):
thename(name),thephoneNumber(phoneNumber) {}

这里的成员变量分别以括号内的实参进行了copy或move构造(再次使用了某个构造函数)。

对于内置类型的成员变量,两种写法的效率差不多,但我们为了一致性最好还是统一使用member initialization list中.
甚至当我们只想用default构造函数时,也可以使用此方法,只需要制定nothing作为初始化实参即可,例如:

1
Person::Person():thename(),thephoneNumber() {};

这种写法的高效性不言而喻,但我们必须记住在member initialization list中不允许遗忘任何一个成员变量。虽然非内置类型都具有自己的default构造函数,但是内置类型必须要手动初始化。
有时候即使成员变量是内置类型,也一定得使用初值列,比如const或者reference的成员变量必须初始化,且前者无法被赋值。
总之,我们只需要记住,对于自定义类型,我们在构造函数中总是使用member initialization list。


初始化次序

自定义类型的初始化次序

c++有着固定的初始化次序:base class总是先于derived class被初始化,而class中的成员变量总是按照声明的次序初始化。
有可能初值列中的次序不是和声明次序一致,但是初始化的顺序却是始终按声明顺序完成的。因此我们最好严格地按照声明次序来写初值列。


“不同编译单元内定义之non-local static对象”的初始化次序

static对象

static对象的寿命从被构造出来直到程序结束为止,因此stack和heap-based对象都不是static。
static对象包括global对象,定义于namespace作用域内的对象、在classes内、以及在file作用域内被声明为static的对象。
函数内的static对象被称为local-static对象(对函数而言是local),其他static对象称为non-local static对象。
程序结束时static对象会被自动销毁,也就是说它们的析构函数会在main()结束时调用。

编译单元

所谓的编译单元,指的是产出单一目标文件的那些源码。基本上它是单一源码文件加上其引入的头文件。

问题描述

我们现在所关心的问题至少涉及两个源码文件,每一个至少包含一个non-local static对象。
这个问题是这样的:某个编译内的某个non-local static对象的初始化动作使用了另一个编译单元内的某个non-local static对象。后者可能在前者之后初始化,这将导致问题的发生。

问题实例

假设有一个FileSystem class,它的作用是让互联网上的文件看起来坐落于本机,由于这个class的作用在于构建一个单一文件系统,因此其产出的对象可能会位于global或namespace作用域内:

1
2
3
4
5
6
7
class FileSystem{
public:
...
size_t numDisks() const;
...
}
extern FileSystem tfs;

假定某些客户建立了一个class以处理FileSystem的目录,那将不可避免地需要使用tfs:
1
2
3
4
5
class Directory{
public:
Directory(parms) {disks=tfs.numDisks();}
...
}

当客户调用该构造函数时,可能tfs尚未被初始化,关键在于如何保证这两个不同源码建立的对象具有初始化的先后次序?

问题解决方案

将每一个non-local static对象搬到自己的专属函数中(在函数内声明为static),这些函数返回一个reference指向它所含的对象。用户调用这些函数,而不是直接指涉对象。换句话说,我们用local static对象替换了non-local static对象。这是singleton模式的一个常见实现。
具体代码如下:

1
2
3
4
5
class FileSystem {...}
FileSystem& tfs(){//具体实操可见More Effective C++ 26
static FileSystem fs;
return fs;
}

这个方法的基础是:函数内的local static对象会在函数被调用期间或者是首次遇到定义式时初始化。所以你在使用函数调用时必然会保证reference指向了一个经历了初始化的对象。而且更棒的是,如果你不调用,则不会触发构造和析构操作。这是真正的non-local static对象所不具备的。

单例模式的纰漏

此操作在多线程下可能会引发问题,处理它的方式是在单线程启动阶段手工调用所有reference-returning函数。


总结

  1. 内置类型必须手工初始化
  2. 使用初值列代替构造函数中的赋值操作。初值列的次序应该与声明次序相同。
  3. 对于跨编译单元之初始化次序问题,请使用local static来代替non-local static。

5.区间成员函数优于与之对应的单元素成员函数

Posted on 2018-04-07 | In Effective STL

前言

如果我们试图将一个vector1内全部元素替换为vector2的后半部分,最好的办法是这样:

1
v1.assign(v2.begin() + v2.size() / 2, v2.end());

assign是一个很好用的函数,当我们想把容器复制到另一个类型相同的容器,operator=是一个好选择,但如果我们试图在不同的容器之间进行拷贝,或者给容器一组全新的值时,assign更优。


区间函数

区间成员函数的特点是其形参是一对表示区间的迭代器。如果不用这种区间,我们往往需要编写循环,但这样效率很低。如果拒绝编写循环,我们可能会使用泛型算法来前言中提到的问题,比如说copy算法:

1
2
3
4
5
6
7
8
9
//循环写法
vector<Widget> v1, v2;
v1.clear();
for (auto ci = v2.begin() + v2.size() / 2;ci != v2.end();++ci){
v1.push_back(*ci);
}
//copy算法
v1.clear();
copy(v2.begin() + v2.size() / 2, v2.end(), back_inserter(v1));

但是实际上copy里面必然也包含循环,而且,所有利用插入迭代器(inserter,back_inserter,front_inserter)来限定目标区间的copy调用,其实都应该替换为对应区间版本函数的调用,比如上文的copy,可以替换为利用区间的insert版本
1
v1.insert(v1.end(), v2.begin() + v2.size() / 2, v2.end());

与copy相比,代码略微简洁,但是最关键的是它直截了当地指出了发生的事情:插入。
STL滥用copy的情况应该被避免,所以再次重申:通过利用插入迭代器的方式来限定目标区间的copy调用,不如直接被替代为对区间成员函数的调用。
区间成员函数在可阅读性上的优点可以被总结为:

  1. 代码简洁
  2. 意图清晰直接

性能优劣分析

我们会发现单元素成员函数比使用区间成员函数需要更多地调用allocator,更频繁地复制,以及更多的冗余操作。举例如下:
假定我们需要将一个int数组复制到vector的前端,如果使用insert函数:

1
2
3
4
int data[numValues]; 
vector<int> v;
...
v.insert(v.begin(), data, data + numValues);

如果通过循环显式地调用insert:
1
2
3
4
5
vector<int>::iterator insertLoc(v.begin());
for (int i = 0; i < numValues; ++i) {
insertLoc = v.insert(insertLoc, data[i]);
++insertLoc;
}

这里值得注意的是,每一次都需要在insert后更新insertLoc,否则会出现2点问题,
1. insertLoc失效,其插入行为不可预料
2. 即便不失效,插入总是发生在begin处,即倒着把data插入到最前面.

如果使用copy算法:

1
copy(data, data + numValues, inserter(v, v.begin()));

当copy模版被实例化后,其代码与基于循环的代码几乎完全相同。因此,我们在分析效率时,只需要分析循环那个版本。
总的说来,一共有3处影响了效率。

  1. 不必要的函数调用
    n个元素的插入必然调用了n次插入操作,而insert只有一次调用,虽然内联能解决这个问题,但编译器不一定会给你内联。
  2. 将v中已有的元素频繁地向后移动
    如果内部元素是自定义类型,则会造成频繁地使用赋值操作运算符和拷贝构造函数(之前都是赋值,最后一个拷贝)
    假设原有容器中有m个元素,需要插入n个元素,一共需要mn次调用:(m-1)n次赋值操作符,n次拷贝构造函数。而insert总是一步到位地将现有元素直接放到最终位置。总代价包括m次移动,n次拷贝构造函数。
    虽然insert几乎总是一次性地移动所有元素到位,但其实它是建立在已知两个迭代器之间距离的基础上,而这个功能是由前向迭代器提供的。也就是说,当传入区间的是输入迭代器例如istream_iterator之类,insert就失去了性能上的优势
  3. 多次扩容
    我们都知道如果你插入时vector的容量已满,它会进行扩容,此时又会复制一大波元素到新的位置。如果插入次数很多,那么扩容的次数也很多,因此,造成了大量的浪费。insert因为已知需要多少容量,因此减少了浪费。
    以上说法对于vector和string同样有效。deque内存管理方式不同,所以内存重复分配的话题对它无效,但是反复移动与多次调用的结论仍然有效。list使用区间形式也有其优势,减少函数调用次数的优势依然存在,虽然list无需反复移动和构造,也不存在内存分配,但是存在对节点的next与prev指针重复多余的操作。一般而言,每一次插入都会导致节点的next与prev赋值一次,但如果我们提前知道插入了多少节点,就避免了重复指针赋值。

哪些函数支持区间操作

区间构造

所有标准容器都提供这种形式的构造函数:

1
container::container(InputIterator begin,InputIterator end);

但如果传给构造函数的是istream_iterator或者istreambuf_iterator时,c++的分析机制会把这条语句解释为函数声明,而不是新建对象。

区间插入

所有标准序列容器都提供这种形式的insert:

1
void container::insert(iterator position, InputIterator begin,InputIterator end);

关联容器使用比较函数来决定元素要放在哪里,所以不能指定位置:
1
void container::insert(lnputIterator begin, InputIterator end);

对于插入,不要忘了很多函数其实做着与insert一样的事情,当你看到反复的push_back或者push_infront时,不妨提供给他们区间形式的insert版本。

区间删除

STL容器都支持区间删除,但是关联与非关联的返回值不同。
序列容器如下:

1
iterator container::erase(iterator begin, iterator end);

关联容器如下:
1
void container::erase(iterator begin, iterator end);

据说因为关联容器返回一个迭代器(指向被删除元素之后的元素)会造成难以负担的性能影响,所以关联容器不反回迭代器。

区间赋值

所有标准列容器都提供了区间形式的assign:

1
void container::assign(InputIterator begin, InputIterator end);


总结

区间函数的三大优点:

  1. 代码简洁
  2. 目的明确
  3. 效率更高

4.调用empty而不是检查size==0

Posted on 2018-04-06 | In Effective STL

​
理论上而言,这二者等价,但是优先使用empty的理由很简单:
对于所有标准容器,empty都是常数时间操作,而某些list的size耗费线性时间。

list独有的splice操作导致了这种情况,举例而言:

1
2
3
4
5
6
7
list<int> list1;
list<int> list2;
...
//将list2中从第一次出现5到第二次出现10的所有节点移动到list1的末尾
list1.splice(list1.end(), list2,
find(list2.begin(), list2.end(), 5),
find(list2.rbegin(),list2.rend(), 10).base());

这段程序只有在list2中在含5的节点之后含有10的节点时才工作。
关键在于list1中有多少个元素?应该是list1之前的元素加上之后链接过来的元素.除非遍历一次,不然编译器不会知道具体个数。
list的设计者主要希望它在插入删除操作中具有较好性能,如果list的size希望有常数时间,那么splice必须遍历自身然后更新size,这样造成了splice具有线性时间。不管怎么说,list总要在size或者splice之间做出取舍。
但是,empty必然是常数时间,那我们为啥不用呢?

4.尽可能使用const

Posted on 2018-04-06 | In Effective C++

const的意义

const与指针

首先复习一下const修饰指针的含义:

如果const出现在*左边,被指物是常量
const出现在*右边,指针自身是常量
两边都有 指向常量的常量指针
至于他们相对类型名的次序则无关紧要

1
2
void f(const Widget* pw);
void f(Widget const* pw);

两种写法的意义相同


const与迭代器

STL迭代器以指针为根据塑造出来,因此iterator就类似于一个T*指针。那么同理,把迭代器声明为const就像把指针声明为const一样(T *const);这意味着该迭代器不得指向不同的东西。
如果你希望迭代器的指向物不可修改,那你应该使用STL中的const_iterator,用它来模拟const T*。


const与返回值

我们应该尽量令函数返回一个常量值,从而在不放弃安全和高效的前提下降低因客户失误而造成的意外。(诸如运算符的返回值应该是一个const对象)


const 成员函数

用const修饰成员的目的是为了确认该成员函数可以作用于const对象。它们有两大重要特性:

  1. 它们使用class接口时我们可以清楚看到哪些函数可以改变函数的内容。
  2. 它们可以操作const对象,这是编写高效代码的关键。因为改善c++程序效率的一个根本办法就是使用pass by reference-to-const方式传递对象。

const成员函数的另一个易被忽视之处在于常量性的不同可以构成重载。
最常见的例子莫过于下标运算符的定义,const的下标运算符返回const引用,普通则返回普通引用。这样则避免了试图修改一个const对象。


成员函数是const意味着什么?

这一问题直接引发了2个流派之争:bitwise constness与logical constness
bitwise constness阵营的人认为成员函数只有在不改变对象内任何一个成员变量的情况下才可以称为const。
这听起来很有道理,但实际上很多不具备const性质的成员函数却能够通过bitwise测试。举例而言,假如一个class内有一个指针,我们通过指针修改指针指向的对象,那么它不会引发编译器异议。最终,我们创建了一个常量对象并对它调用了const成员函数,可是最终它的值还是被改变了。
这种情况导出了logical constness。这一派拥护者指出,一个const成员可以修改所处理对象的某些数据,但只有在客户端检测不出的情况才能如此。这种情况是很直观的,但是编译器只能辨别bitwise constness.为了避免这种无谓的情况,我们可以用mutable来修饰non-static成员变量,在样就算在const成员里我们也可以修改它们。
然而mutable并不能解决所有的问题。至少在以下情况不能:
假设下标运算符不仅需要返回,还需要进行边界检测,扩展边界等操作。显然,这样的下标运算符的const性质十分繁琐,而且伴随着大量的代码重复。
我们真正需要做的是实现const下标运算符的功能,并在non-const函数中调用它,然后再去除它返回对象的const属性。在这里去除const是安全的,因为不管谁调用non-const operator[]都必须要有一个non-const对象。其实现如下:

1
2
3
4
char &operator[](std::size_t position){
return const_cast<char&>
(static_cast<const TextBlocks>(*this)[])
}

很显然,我们先把this转为了const对象,这样就可以调用const版本的下标运算符。再把const函数返回的对象去除了const属性,我们只有const_cast一种方法来实现这种操作。
之所以我们不在const成员函数里调用普通成员函数,是因为这一点及其危险:const成员承诺不修改对象,而non-const成员却可能修改对象。而且要调用non-const函数的话你必须先通过const_cast去掉this的const性质,这简直就是雪崩的前兆。通过non-const函数调用const函数自然很安全。


总结

  1. 将某些东西声明为const可帮助编译器侦测出错误用法。尤其是返回值可以用const修饰。
  2. 编译器强制实施bitwise constness,但编写程序时应该活用mutable,做到logical constness.
  3. 当const函数与non-const函数实现几乎等价时,我们应该在non-const函数中调用const版本以避免代码重复。
<i class="fa fa-angle-left"></i>1…252627<i class="fa fa-angle-right"></i>

xander.liu

266 posts
11 categories
36 tags
RSS
GitHub E-Mail
© 2024 xander.liu
Powered by Hexo
|
Theme — NexT.Pisces v5.1.4