Xander's Wiki


  • Home

  • Tags

  • Categories

  • Archives

  • Search

38.禁止重定义继承而来的默认实参

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

前言

 
我们只能继承virtual和non-virtual函数,而重新定义一个non-virtual函数永远是错误的。所以本节讨论只基于”继承一个带有默认实参的virtual函数”。之所以需要讨论它,是因为virtual函数动态绑定,但默认实参静态绑定。


静态绑定与动态绑定

静态类型与动态类型

对象所谓的静态类型(static type),就是它在程序中被声明为的类型。举例而言:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class shape{
public:
enum color{red,green,blue};
virtual void draw(color c = red) const = 0;
};
class rectangle:public shape{
public:
virtual void draw(color c = green) const;//重定义了默认实参
};
class circle:public shape{
public:
virtual void draw(color c) const;
//当客户通过对象调用此函数时必须使用实参,因为静态绑定不从base继承默认实参
//但若以指针或引用调用则无需如此,因为动态绑定下该函数会从其base获取默认实参
}

此继承体系如图所示:
image_1cbdnpjs21jto1c72j5f1verkd09.png-13.2kB
那么考虑这些指针:
1
2
3
shape* ps;//静态类型为shape*
shape* pc = new circle;//同上
shape* pr = new rectangle;//同上

它们都被声明为pointer-to-shape类型,所以他们的静态类型都是shape*。

动态类型则是指目前所指对象的类型,也就是说,动态类型可以表现出一个对象将会有怎样的行为。
动态类型一如其名,可以在程序执行过程中改变:

1
2
ps=pc;//ps的动态类型改为Circle*
ps=pr;//ps的动态类型改为Rectangle*

动态绑定与静态绑定

virtual函数本身通过动态绑定来执行(也就是说如何执行取决于对象的动态类型),但如果遇到了带有默认实参的virtual函数,这个时候问题来了。
virtual是动态绑定,而缺省参数值却是静态绑定。也就是说,你可能会在调用“一个定义于derived class内的virtual函数”的同时却使用了base class为它指定的缺省参数,举例而言,

1
2
pr->draw();//以为此时的默认实参是green
//但其实等价于调用rectangle::draw(shape::red);

何种原因造成了这种分裂机制

C++将默认实参设为静态绑定的原因在于运行期效率。如果缺省参数设置为动态绑定,那么编译器就必须在运行期为virtual函数决定适当的参数缺省值,而静态绑定在编译期就完成了决定。动态绑定默认实参过于缓慢和复杂。


解决方案

 
就算严格遵守了本节所讲的内容,也未必就称得上最佳设计:

1
2
3
4
5
6
7
8
9
class shape{
public:
enum color{red,green,blue};
virtual void draw(color c = red) const = 0;
};
class rectangle:public shape{
public:
virtual void draw(color c = red) const;//不再重定义
};

这种写法无疑造成了代码重复。更为重要的是,代码重复造成了相依性:一旦作为base class的Shape内的默认实参需要改变,则所有derived class都必须发生改变,否则就造成了重定义默认实参。因此我们必须考虑某种替代手法。

NVI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Shape{
public:
enum color{red,green,blue};
void draw(color c = red) const{
doDraw(c);
}
...
private:
virtual void doDraw(color c) const = 0;
}
class rectangle:public shape{
private:
virtual void draw(color c) const;
};

由于non-virtual不会被覆写,所以derived class继承得到的函数必然具备与base class相同的默认实参。


总结

 
禁止重定义默认实参,因为它们是静态绑定,而virtual函数是动态绑定。

39.判断式必须为纯函数

Posted on 2018-04-19 | In Effective STL

基础概念

 
在开始本节内容之前,有基础概念如下:

  • 判断式是返回bool或者返回值可以隐式转化为bool的东西.举例而言,默认谓词即为判断式.
  • 纯函数是返回值只依赖参数的函数,参数不改变则返回值不变(不具备状态),纯函数中所有的数据只有两种:参数、常量。
  • 一个判断式类是一个仿函数类,它的成员函数operator()返回0或者1,STL算法可以接受一个真正的判断式或一个判断式对象作为谓词。

为什么判断式必须为纯函数

 
前文说过,函数对象在STL算法中pass-by-value。判断式作为一个函数对象,存在另一个设计特性:STL可能会先拷贝它们,然后在需要的时候使用它们的拷贝。这个设计特性的直接反映就是:判别式函数必须是纯函数。

问题实例

下述的判断式类功能为:仅在第三次调用时返回true.

1
2
3
4
5
6
7
8
9
class BadPredicate:public unary_function<Widget, bool> {
public:
BadPredicate(): timesCalled(0) {}
bool operator()(const Widget&){
return ++timesCalled == 3;
}
private:
size_t timesCalled;
};

看起来似乎没毛病。于是我们用它来作为谓词抹除容器内的第三个元素:
1
2
vector<Widget> vw;
vw.erase(remove_if(vw.begin(),vw.end(),BadPredicate()), vw.end());

事实上调用完成后不仅仅会抹除第三个元素,第六个元素也会被抹除。

问题剖析

上述问题的产生原因在于remove_if的实现:

1
2
3
4
5
6
7
8
9
template <typename FwdIterator, typename Predicate>
FwdIterator remove_if(FwdIterator begin, FwdIterator end, Predicate p){
begin = find_if(begin, end, p);
if (begin == end) return begin;
else {
FwdIterator next = begin;
return remove_copy_if(++next, end, begin, p);
}
}

显然,判断式p的copy先被传递给find_if,再被传递给remove_copy_if.
首先,我们构造了一个匿名判断类对象,并用它初始化了判断类对象p,然后p被拷贝到findif中(time被初始化为0),然后调用了3次以后,p的拷贝的time增长为3,返回true.控制权回归remove_if,然后remove_copy_if拷贝了p,此时time为0,(而不是3),因此到第六个元素的时候又触发了true。

解决方案

最简单的方法是把operator()函数声明为const,如此一来,你的编译器不会通过任何改变任何数据成员的指令。
但这是不够的,因为const成员函数还是能改变mutable成员,非const局部静态对象,非const类静态对象、命名空间中的非const对象,以及非const的全局对象。
总的来说,一个行为正常的判断类operator()必然是const的,但还有更严格的要求:必须是纯函数。

37.禁止重定义继承而来的non-virtual函数

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

前言

 
假设我们有base class与derived class如下:

1
2
3
4
5
class B{
public:
void mf();
}
class D:public B {...}

我们应该确定:
1
2
3
D x;
B* pB = &x;pB->mf();
D* pD = &x;pD->mf();

两次函数调用的行为应当一致。


问题实例

 
仍以前文为例,如果我们在D中重定义了mf,也就是说造成了遮蔽名称的现象,那会有如下情况发生

1
2
3
4
5
D x;
B* pb= &x
D * pd = &x;
pb->mf();//调用B::mf();
pd->mf();//调用D::mf();

造成如上行为的原因是因为non-virtual静态绑定,具体来说,如果pb是一个pointer to B,通过pb调用的non-virtual函数永远是B的版本。
但如果mf是一个动态绑定的virtual,那么无论通过pb还是pd调用得到的都是D::mf().


non-virtual被重定义的危害

 
上述实例表明,在发生了non-virtual重定义的情况下,D对象的行为出现了不一致性。具体来说,当mf被调用时,对象的行为可能表现为基类也可能表现为派生类,其决定因素不在于对象自身而在于“指向该对象的指针”的声明类型。

前文已述,public继承代表is-a,而non-virtual代表了class的某种不变性。如果derived class重定义了non-virtual函数,设计便出现了矛盾:如果D真的有必要实现出与B不同的non-virtual函数,也就表明其特异性超过了不变性,这也就违背了is-a关系。

base class的析构函数之所以要声明为virtual也是如此,因为derived class绝不应该重新定义一个继承而来的non-virtual函数。

38.仿函数类与值传递

Posted on 2018-04-18 | In Effective STL

前言

 
c与c++都不允许把函数作为参数传递给其他函数,所以我们传递给函数的是函数指针。比如下述的qsort声明:

1
void qsort(void *base, size_t nmemb, size_t size,int (*cmpfcn)(const void*, const void*));

函数指针是值传递的。


函数对象

 
STL函数对象是函数指针的一种抽象形式,所以按照惯例,函数对象也是按值传递的,最好的证明就是for_each算法:

1
2
template<class InputIterator,class Function>
Functionfor_each(InputIterator first,InputIterator last,Function f);//pass-by-value

显然,函数对象是一个copy,返回值也是一个copy.
其实函数对象不一定非要是值传递(显式地写为引用传递),但对于STL用户而言,这一条必须遵守,否则某些算法甚至无法编译。

函数对象的特点

既然函数对象是值传递的,那它必须满足两条属性:

  • 足够小,易于copy
  • 单态,否则拷贝时会造成割裂

函数对象的实现

因为不满足上述两条要求就不用仿函数类是愚蠢的,实际上我们有的是办法让大的或者是多态的函数对象以值传递的方式进入STL.
具体方法是:把所需的数据和虚函数从仿函数类中分离出来,放到新的类中;然后在仿函数类中包含一个指针,指向这个新类的对象。
例如,我们需要建立一个包含了大量数据并且使用了多态性的函数子类:

1
2
3
4
5
6
7
8
template<typename T>
class BPFC:public unary_function<T, void> {
private:
Widget w;//存有大量数据
Int x;
public:
virtual void operator()(const T& val) const; //虚函数
};

这样把BPFC作为仿函数类是肯定不行的,正确做法是建立一个包含一个指向实现类的指针的小而单态的类,然后把所有数据和虚函数放到实现类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
template<typename T> 
class BPFCImpl:public unary_function<T, void> {
private:
Widget w;
int x;
...
virtual ~BPFCImpl();
virtual void operator()(const T& val) const;
friend class BPFC<T>;//友元类
};
template<typename T>
class BPFC:public unary_function<T, void> {
private:
BPFCImpl<T> *pImpl;
public:
void operator()(const T& val) const{
pImpl->operator() (val);
}
...
};

显然,虽然BPFC是小而单态的,但是其内部的指针在访问时具备了多态性,并且可以访问大量数据。这种技巧在设计模式中被称为Bridge,但我们一定更熟悉另一个名字:PIMPL;


总结

 
函数对象总是通过值传递,因此他们必须小且单态。

36.考虑virtual以外的选择

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

(本节虽然带有面向对象标签,但描述的是如何跳出面向对象的思维看待问题)

问题实例

 
假定我们在编写一款游戏,其中的角色带有一个血条(int),不同的人计算血量的方式不同,所以把该函数声明为virtual似乎很合适:

1
2
3
4
5
class GameCharacter{
public:
virtual int healthValue() const;
...
};

该函数并未定义为pure virtual,这说明我们有一个缺省方法。
让我们现在跳出面向对象设计的常规轨道,考虑一些其他的实现可能。


Template Method 模式(Non-virtual Interface)

有一些人主张virtual函数必须全是private。以实际问题举例,上个例子保留healthvalue为public non-virtual成员函数,并调用一个private virtual函数来进行实际操作。

1
2
3
4
5
6
7
8
9
10
11
12
class GameCharacter{
public:
int healthValue() const{
...//事前处理
int retVal = getHealthValue();
...//事后处理
return retVal;
}
...
private:
virtual int getHealth() const {...}
};

这一基本设计被称为non-virtual interface(NVI)手法。它是所谓template method设计模式(与c++的templates无关)的独特表现形式。我们习惯于把non-virtual函数称为virtual函数的外覆器(wrapper)。

NVI的优点

NVI手法的一大优点在于wrapper确保了virtual函数在调用前的场景已被设计好,并且调用结束后场景得到了清理。(实例中的注释处)举例而言,这些工作可能包括:锁定互斥器、记录日志、验证约束条件、互斥器解锁等等。如果你让客户直接调用virtual,那么可能没办法做好这些事。
在这里需要注意的是:NVI手法涉及在dc内重新定义了private virtual函数。重定义virtual函数表示“某事应当被如何完成”,外覆器的non-virtual属性则表示“某事应当何时完成”(函数何时被调用),这二者并不冲突。

NVI的不足

在NVI手法下不一定virtual一定得是private,它们也可以是portected.(详见Effective C++ 28 Windows实例)有时候virtual甚至必须是public(具有多态性质的bc的析构函数),这么一来就不能实施NVI手法了。


Strategy模式(Function Pointers)

 
NVI手法本质上还是在用virtual函数,只是调用时颇为巧妙而已。针对游戏实例,另有一些人主张“人物血条的计算与人物类型无关”,这种计算不需要“人物”成分。
例如我们可能会要求每一个人物构造函数接受一个指针指向血条计算函数,我们调用该函数进行实际操作:

1
2
3
4
5
6
7
8
9
class GameCharcter;
int defaultHealthCalc(const GameCharacter &gc);//默认计算函数
class GameCharacter{
public:
typedef int (*HealthCalc)(const GameCharacter&);
explicit GameCharcter(HealthCalc hc = defaultHealthCalc):HealthFunc(hc) {}
private:
HealthCalc healthFunc;
}

Strategy的优点

上述做法是strategy设计模式的一个简单应用,和普通的virtual函数相比,其优点如下:

  • 同一人物类型的不同实体可以有不同的计算函数(构造时绑定不同的函数指针)。
  • 血条计算函数可以在人物的不同时期发生变更(比如设置个setHealthcalc函数接受一个函数指针并且替换私有成员)。

Strategy的缺点

在Strategy模式下,血条计算函数已经不再是gamecharacter继承体系内的成员函数,也就是说,它将无法访问对象的non-public部分。
解决方法只有弱化class的封装:

  • 将函数声明为friends
  • 为其需要的数据提供一个public访问接口

至于strategy模式的优点(每个对象拥有各自的函数以及动态更改的能力)能否抵消缺点(降低class的封装程度),必须根据设计情况的不同进行抉择。


Strategy模式(Function)

 
为什么血条计算必须得是一个函数呢?它完全可以是一个类似函数的对象。如果我们使用function对象,很多刚才提及的约束一下子就消失了。funcion对象持有一切可调用物(函数指针、函数对象、成员函数指针),因此刚才的设计可以改为:

1
2
3
4
5
6
7
8
9
10
11
12
class GameCharcter;
int defaultHealthCalc(const GameCharacter &gc);//默认计算函数
class GameCharacter{
public:
function<int (const GameCharacter&)> healthCalcFunc;
explicit GameCharcter(healthCalcFunc hcf = defaultHealthCalc):HealthFunc(hcf) {}
int getHealthValue() const {
return healthFunc(*this);
}
private:
healthCalcFunc healthFunc;
}

和前一个相比,这个设计几乎完全一样,唯一的不同之处是如今class持有的是一个function对象,相当于一个指向函数的泛化指针。然而这个改变为客户带来了更加惊人的弹性(原书中使用了bind增加了形参)也就是说,如今的设计允许客户在计算人物健康指数时使用任何兼容的可调用物。


传统Strategy模式
 
传统Strategy模式更倾向于将当前virtual函数做成一个分离的继承体系中的virtual成员函数,其设计结果大概像这样:
image_1cbc0cklns971g0h14mt1fed1h3s9.png-24.5kB
其核心在于GameCharacter与HealthcalcFunc成为了base class,并且Gamecharacter中含有一个指针,指向HealthcalcFunc继承体系中的对象。具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class GameCharacter;
class HealthCalcFunc{
public:
...
virtual int calc(const GameCharacter& gc) const {...};
...
}
HealthCalcFunc defaultHealthCalc;
class GameCharacter{
public:
explicit GameCharacter(HealthCalcFunc* phcf = &defaultHealthCalc)
:pHealthC(phcf) {}
int healthValue() const{
return pHealthCalc0>calc(*this);
}
...
private:
HealthCalcFunc* pHealthCalc;
};


总结

 
当你为解决当前问题而寻找设计方法时,不妨考虑virtual的以下几个替代方案:

  • NVI手法,template method设计模式的特殊形式。
    它以public non-virtual成员函数包裹较低访问性(private、protected)的virtual函数。
  • 将virtual函数替换为函数指针
  • 将函数指针变成function对象,以上两种都是strategy的某种表现形式。
  • 将继承体系内的virtual换成另一个继承体系内的virtual,这是strategy设计模式的传统实现。

面向对象固然博大精深,但这个世界上还有许多条路值得我们去探索。

37.了解accumulate与for_each

Posted on 2018-04-18 | In Effective STL

accumulate

有些算法可以帮助我们将一个区间提炼成一个数(或一个对象),诸如count_if,或者min_element和max_element.
但有时候我们需要自定义统计的方式,比如说字符串长度求和,指定区间内乘积之类。stl中存在类似的算法,名为accumulate.值得注意的是,它并不在algorithm中,而是在numeric中。

accumulate的存在形式

标准求和

此形式接受一对迭代器和一个初值:

1
2
list<double> ld; 
double sum = accumulate(ld.begin(), Id.end(), 0.0);

值得注意的是这里初始值是0.0而非0,不然的话accumulate会计算double累加的结果,并将最终结果转为一个int.

另外,accumulate只需要输入迭代器,那也就是说istream_iterator和istreambuf_iterator也可以使用:

1
2
3
cout << "The sum of the ints on the standard input is" 
<< accumulate(istream_iterator<int>(cin), istream_iterator<int>(),0);
//建议迭代器命名,原因详见Effective STL 6

自定义统计(谓词放在最后)

字符串长度求和

为了统计各个字符串长度的和,我们首先写下自定义统计函数:

1
2
3
string::size_type stringLengthSum(string::size_type sumSoFar, const string& s){
return sumSoFar + s.size();
}

对于标准库容器,size_type等价于size_t.
长度求和具体实现如下:
1
2
set<string> ss; 
string::size_type lengthSum =accumulate(ss.begin(), ss.end(), 0, stringLengthSum);

指定区间内的数值乘积

1
2
vector<float> vf; 
float product =accumulate(vf.begin(), vf.end(),1.0f, multiplies<float>());

for_each

for_each是一个对区间内所有元素进行某种操作的算法,我认为配合lambda使用效果绝佳。(需要注意的是它返回的执行操作之后的元素的副本,也许使用引用好一些?)

35.区分接口继承与实现继承

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

前言

 
public继承概念其实由两部分组成:函数接口(function interface)继承与函数实现(function implementation)继承。这两种继承的差异类似于函数声明与函数定义的差异。


接口继承与实现继承

具体含义

public继承体系下derived class无非存在以下三种可能:

  1. 只继承base class成员函数的接口(也就是声明)
  2. 同时继承接口与实现,并且能够覆写(override)它们所继承的实现
  3. 同时继承接口与实现,但不允许覆写任何东西

问题实例

以下有一个绘图程序:

1
2
3
4
5
6
7
8
class shape{
public:
virtual void draw() const=0;
virtual void error(const string &msg)
int objectID() const;
}
class rectangle:public shape {}
class ellipse:public shape {}

可以看到抽象类shape内部共有三个声明方式各不相同的函数,draw是一个pure virtual函数,error是一个impure virtual函数,objectID是个non-virtual函数。


函数声明对继承的影响

pure vitual

pure virtual函数有两个最突出的特性:

  • 必须被任何继承了它们的具象class重新声明
  • 通常在abstract class中没有定义

综上,有结论如下:声明一个pure virtual的目的是为了让derived classes只继承函数接口。它等价于在对一个具象derived class要求:“你必须提供该函数,但我并不干涉你如何实现。”

实际上,pure virtual函数也可以有定义,但你只能通过“调用时明确指出其class名称”来调用它,例如:

1
2
shape *ps = new shape;
ps—>shape::draw();

这种写法一般仅有一个用途:为impure virtual函数提供更平常更安全的缺省实现。


impure virtual

impure virtual函数的目的在于:让derived class继承该函数的接口与缺省实现。它等价于在告诉derived class的设计者,“你必须支持该函数,如果你不想自定义的话,那当前存在一个缺省版本。”

隐含风险与解决方案

允许impure virtual函数同时指定函数声明和函数缺省行为带有一定的风险。

问题实例

假定航空公司共有AB两种飞机,二者以相同方式飞行,因此继承体系如下:

1
2
3
4
5
6
7
8
9
10
11
class Airport{...};//机场
class Airplane{
public:
virtual void fly(const Airport& derstination);
...
};
void Airplane::fly(const Airport& destination){
...//缺省,将飞机飞往目的地
}
class ModelA:public Airplane {...};
class ModelB:public Airplane {...};

截至目前为止,这个继承体系是完全正确的。但假设该航空公司又购买了一款新式飞机C,其飞行方式与AB不同。但由于急着令飞机上线,忘记了重定义其fly函数:
1
2
3
class ModelC:public Airplane {...};
Airplane* pa = new ModelC;
pa->fly(airport);

上述程序试图用AB飞机的飞行方式来驱动C,无疑会造成雪崩。

问题剖析

此问题的发生并不在于Airplane::fly()具备缺省行为,而在于ModelC在未表明自己需要的情况下就继承了该缺省行为。因此我们的程序应该完成这个功能:“提供缺省实现给derived class,但只有他们明确要求继承该缺省实现时才会继承”

解决方案
protected defaultFly

此方法的原理在于切断“virtual函数接口”与“缺省实现”之间的连接。

1
2
3
4
5
6
7
8
9
10
class Airplane {
public:
virtual void fly(const Airport& destination)=0;
...
protected:
void defaultFly(const Airport& destination);//函数以protected姿态封装
};
void Airplane::defaultFly(const Airport& destination){
...//缺省,将飞机飞往目的地
}

此时,fly已经被改为一个pure virtual函数,仅仅提供飞行接口。其缺省行为以独立的defaultFly登场,如果想要调用缺省行为,则在继承而来的fly接口中使用inline调用即可:
1
2
3
4
5
6
7
8
9
10
class ModelA:public Airplane{
public:
virtual void fly(const Airport& destination){
defaultFly(destination);
}
...
};
void ModelC::fly(const Airport& destination){
...//自定义飞行模式
}

定义pure virtual函数

有人认为不应该定义defaultFly以污染命名空间,但接口和缺省实现也确实应该分开,于是他们巧妙地通过定义pure virtual函数来避免了这一尴尬:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Airplane {
public:
virtual void fly(const Airport& destination)=0;
...
}
void Airplane::fly(const Airport& destination) {
//缺省行为
}
class ModelA:public Airplane{
public:
virtual void fly(const Airport& destination){
Airplane::fly(destination);
}
...
};
void ModelC::fly(const Airport& destination){
...//自定义飞行模式
}


non-virtual

该函数的不变性(invariant)凌驾于特异性(specialization),因此它表示无论derived class表现地多么奇特,该函数行为都不可改变。声明non-virtual是为了令derived class继承函数的接口及一份强制实现。


继承时易犯的错误

  • 将所有函数声明为non-virtual
    除非你的class不作为base class,否则别这样作死,该用virtual就用,别过多担心其带来的效率问题。(除非在profiler发现真的是它们影响了效率)
  • 将所有函数声明为virtual
    除非你在写interface class,否则该有不变性就应该大胆地说出来。

总结

  1. 接口继承不同于实现继承。在public继承下,dc总是继承bc的接口。
  2. pure virtual只继承接口。
  3. impure virtual为继承指定接口及default实现。
  4. non-virtual继承指定接口,并强制性继承实现。

36.自定义copy_if算法

Posted on 2018-04-18 | In Effective STL

(C++ 11 已经引入copy_if,本节虽过时,了解copy_if的实现也是好的)

前言

 
STL的一大奇特之处在于有11个关于copy的算法,但却没有名为copy_if的算法,也就是说如果你仅仅想要复制区间内某个符合条件的值的话,你得自己动手。
image_1cbb8bpl0tsj1ho01qaiaj9bqf9.png-27.2kB


问题实例

 
假设我们有一个谓词判定widget是否有缺陷,然后把一个vector内所有有缺陷的Widget放进cerr,如果我们有copy_if的话:

1
2
3
bool isDefective(const Widget& w);
vector<Widget> widgets;
copy_if(widgets.begin(), widgets.end(),ostream_iterator<Widget>(cerr, "\n"), isDefective);


copy_if的具体实现

错误版本

大神们认为实现copy_if是微不足道的琐事,但这并不意味着对我等来说实现起来很容易,比如说下面这个(不正确的)具体实现:

1
2
3
4
5
template<typename InputIterator,typename OutputIterator, typename Predicate>
OutputIterator copy_if(InputIterator begin,InputIterator end,
OutputIterator destBegin, Predicate p){
return remove_copy_if(begin, end, destBegin, not1(p));
}

上述实现并不能用于函数中,因为not1不能直接作用于函数指针,最起码也得用一个std::function修饰一下。但算法不应该要求仿函数具有可适配性。

真正实现

1
2
3
4
5
6
7
8
9
template<typename InputIterator, typename OutputIterator, typename Predicate>
OutputIterator copy_if(InputIterator begin,InputIterator end,
OutputIterator destBegin,Predicate p) {
while (begin != end) {
if (p(*begin)) *destBegin++ = *begin;
++begin;
}
return destBegin;
}

34.避免遮蔽继承而来的名字

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

前言

 
本节内容与继承相关性不大,重点基本上在作用域(scope)方面。


名称遮蔽规则

 
在下述代码中,

1
2
3
4
5
int x;//global变量
void doSth(){
double x;//local变量
...
}

作用域形势如下图所示:
image_1cb6lbv3l58g189p1oi8ndo1kqh9.png-9.5kB
当编译器遭遇名称x时,它会首先在doSth的作用域内搜寻,如果找到就不再查找。
c++的name-hiding rules只做一件事:遮掩名称。至于名称所对应的类型,它并不关注。


继承体系下的名称遮蔽规则

 
我们都很清楚当一个derived class成员函数内refer to base class内的某物(成员函数,typedef,成员变量)时,编译器可以找出我们所refer to的东西,这是因为derived class继承了声明于base class内的所有东西。
实际上的运作方式是,derived class的作用域被嵌套在base class作用域内。它们的关系如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Base{
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf2();
void mf3();
...
};
class Derived:public Base{
public:
virtual void mf1();
void mf4();
...
};

image_1cb6lluob1dis16fq1vg41bk11jdtm.png-18.3kB
可以看出类中有着复杂的类型,但名称遮蔽体系对类型什么的根本无感。
假设derived class中的mf4实现如下:
1
2
3
4
5
void Derived::mf4(){
...
mf2();
...
}

当编译器处理到mf2时,必须了解它refer to的是何种东西。于是它按照以下顺序一一搜寻:

  1. mf4所覆盖的local作用域
  2. Derived class作用域内
  3. base class中作用域
  4. 含有base的namespace
  5. global作用域

实例分析

 
考虑如下程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class base{
private:
int x;
public:
virtual void mf1()=0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
};
class derived:public base{
public:
virtual void mf1();
void mf3();
void mf4();
};

image_1cb6m4s0m1e5ai8b15s7o481l1013.png-17.8kB
因为名称遮蔽原则的作用,base class内部所有名为mf1与mf3的函数都被derived class内的mf1函数与mf3函数遮蔽。我们可以认为,此时base中的mf1与mf3不再被继承。(无论它们的参数如何,是否virtual)
这些行为背后的基本理由是为了防止在程序库或者应用框架内建立新derived class时附带地从疏远的base class继承重载函数。
不幸的是,如果我们不继承这些重载函数,那就违反了is-a原则。


解决方案

using语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class base{
private:
int x;
public:
virtual void mf1()=0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
};
class derived:public base{
public:
using Base::mf1;//让bc中一切名为mf1,mf2的一切东西在dc作用域内可见
using Base::mf3;//且为public可见度
virtual void mf1();
void mf3();
void mf4();
};

image_1cb6mrf3l13m0ku2tog17va6dr1g.png-19.2kB
这种操作意味着如果继承了带有重载函数的base class,而我们又希望重定义或override其中的一部分,我们必须使用using语句引入它们,否则某些你希望继承的名称将会被覆盖。


转交函数

有时候我们并不想继承bc的所有函数,这可以理解,但在public继承中绝无可能。(同时这也是using放在dc的public部分的原因:bc的public名称在publicly dc中也应该是public)
但是在private继承体系中是可能并且有意义的。假如dc以private形式继承bc,而它只是想继承那个mf1函数的无参数版本,此时我们有了新的方案::转交函数(forwarding function)

1
2
3
4
5
6
7
8
9
10
11
12
13
class base{
public:
virtual void mf1()=0;
virtual void mf1(int);
}
class derived:private base{
public:
virtual void mf1(){base::mf1();}//转交函数
};
...
Derived d;
d.mf1();
d.mf1(5);//error!Base::mf1()被遮蔽

至此,我们已经讲完了继承和名称遮掩的完整故事,但是一旦继承结合了templates,我们又将面对“继承名称被遮掩”的新形式。


总结

 

  1. dc内的名称会遮掩bc内的名称,在public继承体系中此行为等于作死。
  2. 为了让被遮蔽的名称重见天日,我们可以使用using语句或者转交函数。

35.mismatch与lexicographical_compare

Posted on 2018-04-16 | In Effective STL

前言

 
一个STL菜鸟最常问的问题是“我该如何使用STL来进行忽略大小写的字符串比较?”。这个问题说难很难(考虑国际化问题),说简单也简单(仅设计为strcmp那种样式)。


比较字符

 
在解决字符串比对问题之前,我们需要有一种方法确定两个字符忽略大小写后是否相等。我们仅考虑最简单的版本:

1
2
3
4
5
6
7
int ciCharCompare(char c1, char c2) { 
int Ic1 = tolower(static_cast<unsigned char>(c1));
int Ic2 = tolower(static_cast<unsigned char>(c2));
if (Ic1 < Ic2) return -1;
if (lc1 > Ic2) return 1;
return 0;
}

此函数遵循了strcmp,可以返回1,-1,0三个值。由于各个char的实现不同(可能有符号可能无符号),我们先将其转为uc,然后再将它们转为小写,如此则忽略了大小写区别,最后将比较结果返回。


基于mismatch实现的字符串比对

 
在调用mismatch之前,我们必须确定一个字符串是否比另一个要短,将短字符串作为第一个区间传递。这项工作将由以下函数完成:

1
2
3
4
5
int ciStringCompareImpl(const string& s1, const string& s2);
int ciStringCompare(const string& s1, const string& s2){
if (s1.size() <= s2.size()) return ciStringCompareImpl(s1, s2);
else return -ciStringCompareImpl(s2, s1);
}

在真正的字符串比较函数中,大部分工作由mismatch来完成,它返回一对迭代器,表示两个区间中第一个对应的字符不相同的位置:
1
2
3
4
5
6
7
8
9
int ciStringCompareImpl(const string& si, const strings s2){
typedef pair<string::const_iterator, string::const_iterator> PSCI;
PSCI p = mismatch(s1.begin(), s1.end(), s2.begin(), not2(ptr_fun(ciCharCompare)));
if (p.first== s1.end()) {
if (p.second == s2.end()) return 0;
else return -1;
}
return ciCharCompare(*p.first, *p.second);
}

重点在于函数中使用的not2与ptr_fun.
使用not2是因为mismatch的谓词返回false时算法停止工作,所以我们需要把charcompare返回的0变为1。ptr_function则类似于std::function。


基于lexicographical_compare实现的字符串比对

 
我们还有一个方法就是把之前的charcompare变成一种类似谓词的形式,这样配合算法使用效果更佳,如下所示:

1
2
3
4
5
6
bool ciCharLess(char c1, char c2){//函数对象
tolower(static_cast<unsigned char>(c1)) < tolower(static_cast<unsigned char>(c2));
}
bool ciStringCompare(const string& s1, const string& s2){
return lexicographical_compare(s1.begin(), s1.end(),s2.begin(), s2.end(), ciCharLess);
}

lexicographical_cmp是strcmp的泛型版本。strcmp只对字符数组有效,但前者对任何类型的值的区间都起作用,并且支持自定义比较器。

<i class="fa fa-angle-left"></i>1…192021…27<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