Xander's Wiki


  • Home

  • Tags

  • Categories

  • Archives

  • Search

43.了解typename

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

template声明式

 
在template声明式中使用class与typename有什么不同吗?

1
2
template<class T> class Widget;
template<typename T> class Widget;

答案是没有不同。有的人喜欢用class,因为少打了字,有的人则习惯于使用typename,因为这暗示了用户参数并非一定要是一个class类型。
但这种相同仅仅只是在template声明式内,c++并不总是把class与template视为等价,有时候我们非得使用typename不可。


template内refer to的名称

 
假设有一个template function,接受一个STL容器为参数,容器内持有的对象可被赋值为int。进一步假设这个函数只是打印其第二元素值。我们暂且不去管这个函数有多么地无聊甚至不能编译,其实现如下:

1
2
3
4
5
6
7
8
9
template <typename C>
void print2nd (const C& container){
if(container.size()>=2){
C::const_iterator iter(container.begin());
++iter;
int value = *iter;
cout << value;
}
}

显而易见,这段代码中有两个local变量iter和value。

嵌套从属名称

iter的类型是C::const_iterator,实际上它是什么取决于template参数C.template内出现的名字如果依赖于某个template参数,则称为从属名称。如果从属名称在class内程嵌套状(::)则称之为嵌套从属名称。
value是一个int,不依赖于任何template参数,于是它叫做非从属名称。


嵌套从属名称的风险

嵌套从属名称可能会导致解析(parsing)困难。举例而言,我们如果写了一个比刚才那个更加愚蠢的程序:

1
2
3
4
template <typename C>
void print2nd(const C& container){
C::const_iterator * x;
}

可能有人对这种写法并无异议:我们声明了一个local变量x,它是一个指针,指向C::const_iterator。但如果C中有一个static成员恰好被命名为const_iterator,又或者x恰好是一个global变量。那这个代码就是执行了一次相乘操作。这简直丧心病狂。

解决方案

C++对于这种歧义状态有一种解析规则:如果编译器在template中遭遇了一个嵌套从属名称,它便假设该名称并非类型,除非你主动告诉它这是类型。(这个情况有一个小小的例外)。
想要程序正确运行,上文中的实例应该改为:

1
typename C::const_iterator iter;

任何时候你想在template内指涉一个嵌套从属类型名称,就必须在它之前放上一个typename.(再说一次,有一个小例外)另外,typename只被用来验明嵌套从属类型名称,其它名称没必要用它。

例外情况

typename不可以出现在base classes list内的嵌套从属类型名称之前,也不可以在member initialization list中作为base class修饰符。举例而言:

1
2
3
4
5
6
7
8
9
template<typename T>
class Derived:public Base<T>::Nested{// base classes list中禁止出现typename
public:
explicit Derived(int x)
:Base<T>::Nested(x){//member initialization list中禁止出现typename
typename Base<T>::Nested temp;//需要typename
}
}
}

typename与typedef

由于某些声明式实在太长,所以讲typedef与typename相结合可能是一种不错的主意:

1
2
3
4
template<typename IterT>
void workWithIterator(IterT iter){
typedef typename std::iterator_traits<IterT>::value_type value_type;
}


总结

  1. 声明template参数时,class与typename意义相同。
  2. 必须用typename标识嵌套从属类型名称,但在base class lists && member initialization list中禁止使用。

44.以成员函数代替同名算法

Posted on 2018-04-20 | In Effective STL

前言

 
有些容器提供了与泛型算法相同名称的成员函数,比如关联容器提供了count、find、lower_bound、upper_bound和equal_range,
而list提供了remove、remove_if、unique、sort、merge、reverse。
我们提倡使用成员函数代替同名算法,第一是因为它们的结合性更好,第二是因为它们更快。


关联容器

set

假设我们需要在一个容纳了百万元素的set中查找727的位置:

1
2
3
set<int> s; // 建立set,放入1,000,000个数据
auto i = s.find(727); //使用find成员函数
auto i = find(s.begin(), s.end(), 727); // 使用find算法

find成员函数消耗对数时间,而泛型算法消耗线性时间。原因在于set基于红黑树,红黑树的查找操作只需要对数时间。(完全平衡树所需要的时间更短,但总体效率差于红黑树)

效率并不是唯一的差别,众所周知,STL算法判同基于相等,而关联容器是基于等价,因此查找算法可能会返回给你不同的结果。为了行为一致,我们应该更倾向于使用成员函数。

map

泛型算法及其不适用于map与multimap,因为它们内部元素是pair,因此count成员函数只统计key匹配的pair数目,find等成员函数也是如此,但是泛型算法会使用相等性测试来统计具有相同键和值的pair对象的个数。如果你只希望检查key,那你只能在vector内部存pair代替关联容器。(Effective STL 23)


list

 
对于list而言,使用成员函数最大的提升在于效率。list的成员函数不进行任何拷贝,它们只是简单地操纵连接list节点的指针。算法和它所使用的时间复杂度相同,但是算法需要拷贝。
另外,list成员函数的行为可能与算法并不一样,list的remove或unique等函数无需执行erase.
sort算法不能用于list的双向迭代器,merge算法不能修改源范围,但list的同名成员函数可以。

42.了解隐式接口与编译期多态

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

OOP中的接口与多态

 
面向对象编程以显式接口和运行期多态解决问题。举例而言:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Widget{
public:
Widget();
virtual ~Widget();
virtual size_t size() const;
virtual void normalize();
void swap(WidgeT& other);
...
};
void doSth(Widget& w){
if(w.size()>10){
Widget temp(w);
temp.normalize();
temp.swap(w);
}
}

对于doSth中的w,我们可以这样分析:

  1. w被声明为Widget,因此w必须支持Widget接口。我们可以通过打开Widget.h观察接口,所以我们称这种接口为显式接口,即此类接口在源码中明确可见。
  2. 由于Widget的部分成员函数是virtual,因此w对那些函数的调用将表现出运行期多态,即于运行期根据w的动态类型决定调用哪个函数。

隐式接口与编译期多态

 
Templates与泛型编程的世界,与面向对象有根本性的不同。在此世界中,显式接口和运行期多态仍然存在,但是重要性降低。反倒是隐式接口与编译期多态移到了前面。举例而言:

1
2
3
4
5
6
7
8
template <typename T>
void doSth(T& w){
if(w.size()>10){
Widget temp(w);
temp.normalize();
temp.swap(w);
}
}

doSth现在变成了一个函数模板,对于doSth中的w,我们现在可以这样分析:

  1. w必须支持哪一种接口,由template function中执行于w身上的操作决定。以实例而言,size,copy构造函数,normalize等似乎是类型T必须支持的一组接口(其实不然)。这一组(必须能够编译)的表达式可以成为类型T必须支持的一组隐式接口。
  2. 凡涉及w的任何函数调用,例如operator>等等,都有可能造成template具现化。这样的具现行为发生在编译期,“以不同的template参数具现化function templates”会导致调用不同的函数,此即为编译期多态。

编译期多态与运行期多态的对比

 
编译期多态和运行期多态的差异,十分类似于“哪一个重载该被调用”(编译期)与“哪一个virtual函数该被绑定”(运行期)之间的差异。


显式接口与隐式接口的对比

显式接口

通常情况下显式接口由函数的签名式(函数名称、参数类型、返回类型)构成。举例而言:

1
2
3
4
5
6
7
8
9
class Widget{
public:
Widget();
virtual ~Widget();
virtual size_t size() const;
virtual void normalize();
void swap(WidgeT& other);
...
};

Widget的接口包括构造、析构函数,函数size,normalize,swap及其参数类型、返回类型、常量性。当然也包括编译器自动生成的函数,还有typedefs。

隐式接口

隐式接口并不基于函数签名式,而是由有效表达式组成。

问题实例

举例而言:

1
if(w.size()>10 && w!=sth);

T的隐式接口看起来好像有这些约束:

  1. 必须提供一个size函数,该函数返回一个整数值。
  2. 必须支持operator!=函数,用来比较两个T对象。

然而,由于操作符重载带来的可能性,这两个约束都不需要满足。(手动滑稽)

实例剖析

T似乎必须要有一个size函数,但这个函数可能是从base class继承得到的。size也没必要返回一个整数,甚至不需要返回一个数值类型。它只要返回一个类型为X的对象,这个对象配合int内置类型可以调用operator>即可。甚至。。。它都无需获取一个X对象,只要获取一个能够隐式转换为X对象的Y对象即可。
同理,T也没必要支持operator!=。因为下面的情况也是可能的:operator!=接受一对类型为X与Y的对象,我们只要T可以转为X型,sth可以转为Y型即可。
更有甚者,也许连operator&&也已经被重载,左右两边已经变成了不知道什么类型,执行什么操作的东西。

真正的约束条件

如果都按照上文的做法分析,似乎约束条件很难界定,但是实际上还是很简单的:
对于size之类的单个表达式可能约束条件很难界定,但对于整体约束条件却很好判断:

  1. if里面必须是一个bool.
  2. 其他隐式接口,诸如copy构造函数之类,必须确保对T型对象有效。

加诸于template参数身上的隐式接口如同加诸于class对象身上的显式接口一样真实,而且两者都在编译期通过检查。你无法以一种与显式接口矛盾的方法来使用对象,你也无法在template中使用不支持隐式接口的对象,二者都将导致无法编译。


总结

  1. classes与templates都支持接口与多态
  2. classes的接口是显式的,以函数签名为中心,多态发生在运行期
  3. templates的接口是隐式的,奠基于有效表达式。多态的实现则基于templates具现化和函数重载解析,发生于运行期。

43.泛型算法优于显式循环

Posted on 2018-04-20 | In Effective STL

前言

 
算法内部本身就存在循环,因此众多需要用循环实现的任务,算法可以轻松完成。


问题实例

 
假设当前有一个支持重画的class:

1
2
3
4
5
6
class Widget {
public:
...
void redraw() const;
...
};

如果我们试图将某个list内所有Widget重画,其循环版本大致如下所示:
1
2
3
4
list<Widget> lw;
for (auto i = lw.begin();i != lw.end(); ++i) {
i->redraw();
}

当然了,也可以使用for_each算法:
1
for_each(lw.begin(),lw.end(),[](Widget& w){w.redraw();});


泛型算法相较于显式循环的优点

 
一般来说,程序员通常认为循环比算法易读,但本节内容将说明算法更可取,理由有三:

  1. 效率高
  2. 正确性高
  3. 更直观

效率

相较于显式循环,算法的高效性体现在:

  1. 避免了每一次循环结束迭代器都需要与end()作比较(i != lw.end());
  2. 库的实现者可以用更优化的方式来遍历容器。
  3. 大佬们写的算法比我们的实现更高效。

正确性

众所周知,写循环时最麻烦的莫过于确保当前迭代器的有效性,并且保证它指向了正确的位置。

问题实例

假设有一个数组,我们试图将其中的每一个元素都加上41然后插入到deque的前端
,循环体如下所示:

1
2
3
double data[maxNumDoubles];//C API
for(size_t i=0;i<numDoubles;++i)
d.insert(d.begin(),data[i]+41]);

这段代码的问题在于:插入的元素是反序的,因为每一次都把待插入元素放在了开头。
于是我们做出了如下修改:
1
2
3
4
auto insertLocation = d.begin();
for (size_t i = 0; i < numDoubles; ++i) {
d.insert(insertLocation++, data[i] + 41);
}

但是实际上这个更不行,因为insert之后后面的迭代器都失效了,insertlocation自然也是如此,所有行为均未定义。
但最终我们还是解决了这个问题,就是每一次插入后都更新insertlocation…
1
2
3
4
5
auto insertLocation = d.begin();
for (size_t i = 0; i < numDoubles; ++i) {
insertLocation=d.insert(insertLocation, data[i] + 41);
++insertLocation;
}

当然,使用插入迭代器也是不错的主意。

使用算法

刚才我们费了半天周折终于利用显示循环写出了正确的程序,那如果使用算法呢?

1
transform(data, data + numDoubles,inserter(d, d.begin()),[](int i){i+=41;});

直观性

任何一个算法在我们读到其名字时就已经了解了它的行为,但显式循环则不然。


总结

 
相对于泛型算法,显式循环稍逊风骚。但它并非一无是处,使用循环能让人更清楚地明白加诸于迭代器上的操作。
我们可以认为泛型算法提升了软件的抽象层次,并且使得它更容易实现、文档化、增强、维护。

41.多重继承

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

前言

 
一旦谈及multiple inheritance(MI),C++程序员总会分为两个阵营:

  1. single inheritance(SI)不错,MI更好。
  2. SI不错,MI垃圾

多重继承带来的歧义

 
抛开争论,首先我们必须要认清:只要我们在设计中使用了MI,程度便有可能从一个以上的classes继承相同名称,这会引发一些歧义,举例而言:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class BorrowableItem{//允许借阅物
public:
void checkOut();//离开时检查
...
};
class ElectronicGadget{//电子设备
private:
bool checkOut() const;//执行自我检测
};
class MP3player://Mp3是借阅物也是电子设备
public BorrowableItem,
public ElectronicGadget
{...};
MP3Player mp;
mp.checkOut();//歧义产生,改掉用哪个?

为了避免这些歧义,我们应该使用作用域运算符::来指明具体调用哪个版本。例如:
1
mp.BorrowableItem::checkout();


“钻石型”多重继承

 
多重继承是指继承一个以上的base classes,但这些base class一般不作为另一些class 的derived class,因为那会导致”钻石型”多重继承。

问题实例

image_1cbgc9k62aed55ka6o18k2qon9.png-11.6kB

1
2
3
4
5
6
7
class File {...};
class InputFile:public File {...};
class OutputFile:public File {...};
class IOFile:
public InputFile,
public OutputFile
{...};

具体到这个实例,有问题如下:假定File内有一个名为FileName的成员变量,那么IOFile对象内应该有几个此成员呢?它可能会忠实地完全复制从InputFile和OutputFile继承而来的所有成员,从而导致存在两个FileName,也可以只继承其中的一个。

virtual base class 与 virtual继承

C++对上述的两种方案都支持,但默认执行完全复制。如果这并非你所愿,那你必须令带有重复数据的class(也就是File)成为一个virtual base class,同时,你还必须令所有直接继承它(file)的class采用virtual继承。实例如下:
image_1cbgcplh31qd81lsv19o8c62bagm.png-11.4kB

1
2
3
4
5
6
7
class File {...};
class InputFile:virtual public File {...};
class OutputFile:virtual public File {...};
class IOFile:
public InputFile,
public OutputFile
{...};

STL中就有一个结构类似的多重继承体系,只不过其classes是class templates,名称分别是basic_ios,basic_istream,basic_ostream,basic_iostream。

virtual继承的成本(详见More Effective C++ 24)

从行为正确的观点看,public继承应该总是virtual。但是实际上,为了避免继承得来的变量重复,编译器不得不使用一些其它操作(因此付出了成本),使用virtual继承的class所产生的对象一般比用non-virtual继承产生的对象要大,而且访问速度也慢。

virtual继承的成本还体现在其他方面,比如说virtual base classes初始化规则比起non-virtual bases的情况远为复杂而去不直观。virtual base class的初始化责任是由继承体系中的最底层class负责,这意味着:

  1. classes若派生自virtual base class而需要初始化,则其必须认知virtual base—无论他们离的多远。
  2. 当一个新的dc加入继承体系,它必须承担起其virtual base的初始化责任。

对于virtual base(或者说virtual继承),我们应该保证非必要不使用。如果必须使用,则尽力避免在virtual base中放置任何数据,如此则不必担心初始化等等问题。(这不得不让人想起了Java的Interface,它在相当多的情况下兼容于C++中的virtual base class)。


Interface class(详见Effective C++ 32)

问题实例

(课本中接着描述了Person interface与具体实现的class)
如果我们需要用某个类的现有接口,又希望使用另一个类现有的具体实现,无疑,我们可以public from interface && private from implement。


总结

 

  1. 多重继承比单一继承复杂,并且有可能导致歧义性,以及对virtual继承的需要。
  2. virtual继承会增加大小,速度,初始化各方面的成本,但如果virtual base不带有任何数据,其使用颇有价值。
  3. 多重继承确有正当用途,比如public继承interface且private继承implement class.

42.less<T>与operator<

Posted on 2018-04-20 | In Effective STL

问题实例

 
假设当前存在对象Widget,其具备重量与最高速度属性:

1
2
3
4
5
6
7
class Widget {
public:
...
size_t weight() const;
size_t maxSpeed() const;
...
};

我们默认以重量为排序方式,所以Widget::operator<如下:
1
2
3
bool operator<(const Widget& lhs, const Widget& rhs){
return lhs.weight() < rhs.weight();
}

但如果我们想建立一个按照最高速度排序的multiset<Widget>,而我们又知道默认的比较函数是less<Widget>,而默认的less<Widget>又调用operator<工作。那似乎我们必须特化less<Widget>来切断less<Widget>与operator<之间的纽带,让它只关心speed:
1
2
3
4
5
6
template<>//less针对Widget的一个特化
struct std::less<Widget>:public std::binary_function<Widget,Widget,bool> {
bool operator()(const Widget& lhs, const Widget& rhs) const{
return lhs.maxSpeed() < rhs.maxSpeed();
}
};

这种行为是在特化std namespace里面的模板,虽然std禁止了大部分修改操作,但这种自定义模板特化还是允许的。(Effective C++ 26 也执行了这样的操作)
但令less做除operator<以外的事情是对预期行为的无故破坏,就像你不应该把operator+重载成乘法一样。


解决方案

 
STL中没有一个用到less的地方你不能指定一个不同的比较类型。比如说我们可以建立一个名字不叫less的仿函数类:

1
2
3
4
5
6
struct MaxSpeedCompare:public binary_function<Widget, Widget, bool> {
bool operator()(const Widget& lhs, const Widget& rhs) const{
return lhs.maxSpeed() < rhs.maxSpeed();
}
};
multiset<Widget, MaxSpeedCompare> widgets;//自定义比较器

我们不应该切断less<T>与operator<的关联性,如果你确有需要,应当在声明容器时同时声明自定义比较器。

40.private继承

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

private继承的特性

 
private继承的特性可以概括为如下两条:

  1. 如果classes之间的继承关系是private,编译器不会自动将一个derived class对象转换为一个base class对象。
  2. 由private base class继承而来的所有成员,在derived class中都会变成private属性,纵使它们原本是protected或者public.

private继承的意义

 
private继承意味着implemented-in-terms-of。
如果我们让class d以private形式继承class b,这说明我们仅仅只是为了采用某些b中已经存在的特性,而不是b和d有任何观念上的交集。
private继承完全是一种实现技术(这也是为何b中所有东西都被转为了private,因为它们都是实现细节)。如果说public继承代表了接口必须被继承,private继承意味着只有实现部分被继承,接口部分则被隐藏。private继承在软件设计层毫无意义,它的价值体现在软件实现层。


何时使用private继承

 
上一节说过,composition也有implemented-in-terms-of的意义,那如何在composition与private继承之间取舍呢?答案是:尽可能使用复合,只有在必要时(protected成员以及virtual函数牵扯进来)才使用private继承。

问题实例

假设我们有一个Widget,我们需要令它能够记录每一个成员函数的被调用次数,并且在运行期间我们将周期性地检查被调用次数。为了完成这项工作,我们需要一个定时器。假定库中存在着Timer class:

1
2
3
4
5
6
class Timer{
public:
explicit timer(int tickFrequency);
virtual void onTick() const;//时间到后调用该函数
...
}

看起来我们只需要重新定义那个virtual函数就可以指定周期地读取Widget的状态了。

解决方案

privte继承

如果需要重定义virtual函数,Widget必须继承自Timer。它们之间的继承关系肯定不是public继承,因为它们并不满足is-a关系,而且用户也不能对着一个widget对象调用onTick成员函数,因为从观念上而言,该接口并不是Widget的一部分。(接口易用性原则 Effective C++ 18)
所以我们使用了private继承,并且写作如下形式:

1
2
3
4
class Widget:private Timer{
private:
virtual void onTick() const;//重定义函数 读取内部数据
}

这个设计很好,但是并不值钱,因为private继承绝非必要。我们完全可以使用一个composition来取代他。


composition

只要在widget内部声明一个嵌套式private class,后者以public形式继承Timer并且重新定义onTick,最后放一个此类对象在该widget内即可,有设计样稿如下:
image_1cbe3etni1c16pn614dn2ava789.png-8.3kB

1
2
3
4
5
6
7
8
class Widget{
private:
class widgetTimer:public Timer{
public:
virtual void onTick() const;
};
widgetTimer Timer;
};

composition写法的优点

  1. 未来时态编程兼容性(More Effective C++ 32)
    未来Widget可能会派生处derived class,但同时你可能会禁止derived class重新定义ontick。如果Widget继承自Timer,以上做法绝无可能实现。(derived class总可以重定义virtual函数)
    如果Widgettimer是Widget的一个private成员并且继承自Timer,Widget的derived class将无法直接使用WidgetTimer,因此也无法继承或者定义它的virtual函数。(阻止derived class重新定义virtual函数的能力,可见Java与C++中的final)
  2. 降低编译依存性
    如果Widget继承自Timer,那么它必然需要#include Timer.h。
    但如果现在将WidgetTimer移出Widget之外,然后令Widget内含一个指针指向WidgetTimer,(PIMPL)Widget就可以只带着一个简单的声明式,这种解耦对于大型系统而言极为重要。

总之,private继承主要用于“当一个意欲成为dc的class想访问一个意欲成为bc的class的protected部分,或者为了重定义一个或者多个virtual函数”,但此时两个class之间的关系决非is-a而是is-implementef-in-terms-of的关系。


private继承与empty class

private继承可以帮助实现空间最优化,不过只涉及empty class.

empty class

empty class是指不带有任何数据的class,它们没有non-static成员变量,没有virtual函数(因为会导致vptr),没有virtual base class(亦会造成空间额外开销,可见Effective C++ 41、More Effective C++ 24).
从理论上而言,empty class对象不使用任何空间,因为没有任何数据需要存储。但是由于技术上的理由,C++规定凡是独立对象都必须拥有非0大小,

1
2
3
4
5
6
class Empty {};
class HoldAnInt{
private:
int x;
Empty e;//理论上不占据内存
}

你会发现sizeof(HoldAnInt)>sizeof(int),因为sizeof(Empty)=1.
由于“独立对象都必须拥有非0大小”的规定,编译器将一个char进入了空对象内。而后可能存在的齐位等要求导致了Holdanint可能获得了不止一个char的大小。

EBO(empty base optimization)

之前我们说过,独立对象的大小一定不为0,也就是说,这个约束不适用于对象内的base class成分,所以如果令Holdanint继承自Empty,则有

1
2
3
4
5
class HoldAnInt:private Empty{
private:
int x;
}
sizeof(HoldAnInt)==sizeof(int);//true

这也就是所谓的EBO(empty base optimization).此外,EBO只在单一继承时有效。

EBO的作用

可能有人会质疑Empty的作用,实际上在STL中存在着大量的类似Empty的基类,例如unary_function等等(详见Effective STL 40),它们的内部往往内含着有用的typedefs、enums、以及non-virtual函数。正是由于EBO的存在,这些函数保证了derived class的大小不受干扰。


总结

  1. private继承意味着is-implemented-in-terms-of。它通常比composition的级别低,但是如果dc想要访问base class的protected成员或者重定义virtual时,这种设计是合理的
  2. private继承可以实现EBO,这对需要尽力缩减对象大小的开发者来说是一件好事。

41.了解ptr_fun、mem_fun以及mem_fun_ref

Posted on 2018-04-19 | In Effective STL

(个人认为习得lambda后无需理会本节内容)

前言

 
ptr_fun、mem_fun和mem_fun_ref的主要任务是覆盖C++固有的语法矛盾。


问题实例

 
当前存在一个函数f与一个对象x,编码环境不处于x的成员函数内,如果需要在x上调用f,则有三种调用方法:

1
2
3
f(x);//当f是一个非成员函数
x.f();//当f是一个成员函数,并且x是一个对象或一个对象的引用
p->f();//当f是一个成员函数,并且p是一个指向对象的指针

假设有一个测试Widget的函数和一个Widget容器:
1
2
void test(Widget& w);
vector<Widget> vw;

我们需要测试容器内所有的Widget:
1
for_each(vw.begin(), vw.end(), test);

但如果test是一个成员函数,我们似乎可以这样:
1
for_each(vw.begin(), vw.end(),&Widget::test);//无法编译

又或者我们还会试图访问指针来执行操作:
1
2
list<Widget*> lpw;
for_each(lpw.begin(), lpw.end(),&Widget::test);//无法编译


问题剖析

 
for_each不支持将成员函数作为谓词传入,原因在于for_each的实现:

1
2
3
4
template<typename InputIterator, typename Function>
Function for_each(InputIterator begin, InputIterator end, Function f){
while (begin != end) f(*begin++);
}

STL算法中函数和函数对象总是使用用于非成员函数的语法形式调用,这也就是mem_fun与mem_fun_ref存在的意义。它们让成员函数得以作为谓词。


mem_fun与mem_fun_ref

 
它们具体的实现并不复杂,总体来说是一个函数模板,以下是其中的一个声明:

1
2
3
4
//用于不带参数的non-const成员函数
// C是类,R是被指的成员函数的返回类型
template<typename R, typename C>
mem_fun_t<R,C> mem_fun(R(C::*pmf)());

mem_fun带有一个指向成员函数的指针pmf,并返回一个mem_fun_t类型的对象。这个仿函数类容纳成员函数指针并且提供一个operator(),它调用指向传给operator()的对象上的成员函数。

针对Widget那个实例:

1
2
ist<Widget*> lpw;
for_each(lpw.begin(), lpw.end(),mem_fun(&Widget::test));

for_each接受一个mem_fun_t类型的对象,它持有一个test函数指针,对于lpw里面所有的Widget*指针,for_each使用调用mem_fun_t,随后该对象立刻在Widget*上调用test.

mem_fun_ref效果类似,它们也被称为函数对象适配器。


ptr_fun

 
ptr_fun增加了一些typedef,如果你不确定啥时候用它,就记得每一次都用它。

39.通过复合塑造出"has-a"或"根据某物实现出"

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

前言

 
复合(composition)是类型之间的一种关系,其意义为:某种类型的对象内含其它类型的对象。举例而言,Address、Name是独立的class,而Person class将它们作为成员变量,这便是一种复合。


复合

 
众所周知,public继承意味着is-a关系。复合则意味着has-a(有一个)或者is-implemented-in-terms-of(根据某物实现出)。其意义具体是has-a,还是根据某物实现出,取决于你当前软件系统中处理的领域(domain)。

应用域与实现域

在软件设计中,对象有的是真实世界的某些事物在计算机逻辑层面的映射,比如人物,汽车,住址等等,这些对象属于应用域。
有些对象是实现细节上的人工制品,比如缓冲区,互斥器,查找树等等。这些对象属于实现域。
当复合发生于应用域内时,类型之间表现出has-a的关系。当它发生于实现域内时,类型之间则表现出“根据某物实现出”的关系。

关系判定

很少有人会把把has-a与is-a搞混,比如说人有一个地址,而绝非人是一个地址。关系判定的难点在于区分is-a与is-implemented-in-terms-of.

问题实例

假设当前我们希望由list派生出一个set(不基于平衡树实现的理由是此应用场景空间性能比时间性能更重要)我们可能会写出这样的东西:

1
2
template <typename T>
class set:public list<T> {}

这种设计显然是错误的,public继承意味着is-a,也就是每一个set必然也是一个list,但我们都知道list可以存放多个相同元素,set则不行,它们之间绝非is-a关系,因为对list对象成立的某些东西在set对象中并不成立。

问题解决

正确的做法是根据list实现出set,也就是判定它们之间的关系为“根据某物实现出”:

1
2
3
4
5
6
7
8
9
10
template<typename T>
class set{
public:
bool member(const T& item) const;
void insert(const T& item);
void remove(const T& item);
size_t size() const;
private:
list<T> rep;//复合
}


总结

  1. 复合与public继承的意义完全不同
  2. 在应用域,复合意味着has-a。在实现域,符合意味着根据某物实现出。

40.仿函数类的适配性

Posted on 2018-04-19 | In Effective STL

(本节内容未能理解,建议结合STL源码剖析阅读,另外,我还是习惯于使用lambda)

问题实例

 
假设现有一个存放widget*的list,以及一个判断式判断widget*指向的对象是否有趣:

1
2
list<Widget*> widgetPtrs;
bool isInteresting(const Widget *pw);

如果我们想要查找第一个有趣的widget,直接调用find_if即可:
1
2
3
4
auto i = find_if(widgetPtrs.begin(), widgetPtrs.end(),isInteresting);
if (i != widgetPtrs.end()) {
...//i指向了目标对象
}

但问题在于,如果我想要找第一个不有趣的Widget,常规写法遭到了编译失败:
1
auto i =find_if(widgetPtrs.begin(), widgetPtrs.end(),not1(isInteresting)); //error

为了保证成功编译与运行,我们必须对判断式使用ptr_fun:
1
auto i = find_if(widgetPtrs.begin(), widgetPtrs.end(),not1(ptr_func(isInteresting)));


ptr_fun与适配性

 
ptr_fun所做的事情就是使一些typedef生效。作为一个低级的函数指针,isInteresting不具备not1所需要的typedef。
四个标准函数适配器(not1,not2,bind1st,bind2nd)都需要存在某些typedef,我们称缺乏typedef的对象不可适配。
这里只提及缺乏typedef但并没有描述那些typedef是什么,这是因为除非你需要设计自己的适配器,否则你都可以通过继承一个base sturct来获得它们。接受一个参数的谓词继承unary_function,两个的则是接受binary_function。


仿函数模板类

 
unary_function和binary_function是模板,所以必须要指定模板参数.其继承形式大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
template<typename T>
class MeetsThreshold: public std::unary_function<Widget, bool>{
private:
const T threshold;
public:
MeetsThreshold(const T& threshold);
bool operator()(const Widget&) const;
...
};
struct WidgetNameCompare:public std::binary_function<Widget, Widget, bool>{
bool operator()(const Widget& lhs, const Widget& rhs) const;
};

可以看出,operator()的返回值被作为了最后一个模板参数。
一般来说,我们习惯于把无状态类记为struct(也许是为了少打那个public),有数据的类则记为class.
可以看到,operator()的参数是const Widget,但给模板的参数却是Widget,你只需要记得这么做就好了。但如果是指针,一定要记住const不能少,模板参数必须和实参一样。

如果给了一个仿函数类多种调用形式,那必然会减少可适配性。

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