Xander's Wiki


  • Home

  • Tags

  • Categories

  • Archives

  • Search

23.将成员变量声明为private

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

为什么不使用public成员变量

  1. 语义一致性
    如果成员变量都不是public,那我们必然是通过成员函数来访问对象,所以再也不用担心什么时候用接口,什么时候用属性了。
    另外,使用函数可以对变量进行更加精准的控制,你完全可以实现只读,读写,或者不予读写,甚至只写操作。举例而言:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class AccessLevels{
    public:
    ...
    int getReadOnly() const{return readOnly;}
    void setReadWrite(int value) {readWrite=value;}
    int getReadWrite() const {return readWrite;}
    void setWriteOnly(writeOnly=value;}
    private:
    int noAccess;//禁止访问
    int readOnly;//只读
    int readWrite;//读写
    int writeOnly;//只写
    }
  2. 封装性
    将成员变量隐藏在函数接口之后,可以为“所有实现的可能”提供弹性,我们可以在不改变接口的同时更改实现,从而保证了用户只需要在重新编译后即可享有与原先不同的体验。(Effective C++ 31 甚至提出了编译分离)
    封装的重要程度可能比你想的还要更加高一些,如果你对客户隐藏了成员变量,那么至少可以保证class的约束条件总能获得维护,因为只有成员函数可以影响它们,并且开发者保留了日后变更实现的权利。如果不进行封装,那么客户码中将充斥着大量的成员变量,这也意味着我们无法随意地更改原有程序,因为每一次改动都可能造成客户端失效。public意味着不封装,不封装意味着不可改变,因为你一变用户也要跟着变。

protected的封装性

</br>
可能你会认为protected的封装性要高于public,但事实并非如此。因为所有使用了该变量的derived classes都遭到了破坏。一旦你将某个成员变量设为public或protected,并且随后交付客户使用,那你在今后就很难改变该变量所涉及的一切,除非你愿意付出重写、重新测试、重新编文档、重新编译的代价。
从封装的角度而言,访问权限只有两种:private(提供封装)和其他(不提供封装)。


总结

  1. 将成员变量声明为privte。这可赋予客户语义一致性,访问权限控制,并且令class作者拥有充分的实现弹性。
  2. protected并不比public更具封装性

23.考虑用有序vector代替关联容器

Posted on 2018-04-13 | In Effective STL

(我认为本节内容强调的是底层数据结构的选择)

各容器的查找速度

 
理论上,散列容器可以提供常数时间的查找。如果你仅仅需要对数时间的查找,和直觉相悖的是,有序vector性能可能会优于关联容器。


有序vector的优越性

为何有序vector在性能上优于关联容器

关联容器的底层数据结构是平衡搜索二叉树,其设计是综合了插入,删除,查找下的最优,但我们在实际应用中这三种操作并非像在测试条件那样随机执行。一般而言,都是某一段时间都在插入,另一段时间都是在查找等等…在这种情况下,有序vector可能会比关联容器在时间和空间上性能更好。

具体优越点

  1. 空间问题
  2. 引用局部性问题

问题实例

空间优越性

假设关联容器元素类型为Widget,在底层数据结构平衡二叉树中不仅仅存着一个Widget,更是存着左子、右子、父节点这三个指针。
vector不需要存储指针,并且可以通过swap技术可以消除多余的内存需求。空间占用比关联容器少了许多。

引用局部性问题

假设我们有足够多数据结构,那么它们在存储时会分成多个内存页面,vector所占用的页面显然要少于关联容器(空间优越性),但这并不是最关键的。如果STL没有改进关联容器的引用局部性,二叉树节点会分散在各个页面,直接导致了更多的缺页中断。vector则不会如此,因为其内存连续,二分查找时不会发生太多的页面错误。

vector的缺陷

上述所说的查找高效性仅限于vector处于有序状态。此外,对于动态操作,是vector的花销惊人(可能还伴随着扩容),所以只有当查找操作几乎不与插入删除混用时,使用有序vector代替关联容器才有意义。


有序vector与关联容器的替换

替换set

用vector替换set十分自然,仅有在针对mutiset时记住排序应该使用stable_sort.关于具体的查找算法选择,详见Effective STL 45;

替换map

元素类型

当我们试图用vector代替map或者mutimap时,需要注意的是,vector内部必须容纳pair对象。
当我们声明map<K,V>时,map内部其实存储的是pair<const K,V>,由于对vector排序时其值会通过赋值来移动,所以vector模拟map时内部只能是pair<K,V>.

排序函数

map在排序时仅仅使用key来作为排序的依据,那么vector中的pair也一样。这导致我们必须自定义一个pair的比较函数,因为默认版本根据pair的两个组件来排序。(我还没排序过pair)

查找函数

用来排序的比较函数将作用于两个pair对象,但是查找只用到了key值,所以必须给用于查找的比较函数一个key类型的对象和一个pair,关键在于我们并不了解到底是key还是pair作为第一个实参传递,所以需要用两个用于查找的比较函数:一个key值先传递,另一个pair先传递。

比较类型实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef pair<string, int> Data; 
class DataCompare {// 用于比较的类
public:
bool operator()(const Data& lhs,const Data& rhs) const{//用于排序的比较函数
return keyLess(lhs.first, rhs.first);
}
//以下为基于查找的比较函数,因为无法确定是输入次序,因此构成重载
bool operator()(const Data& Ihs, const Data::first_type& k) const {
return keyLess(lhs.first, k);
}
bool operator()(const Data::first_type& k,const Data& rhs) const {
return keyLess(k, rhs.first);
}
private:
//真正调用的比较函数
bool keyLess(const Data::first_type& k1,const Data::first_type& k2) const{
return k1 < k2;
}
};

总结

 
只要数据结构的使用过程确实符合本章所述,并且没有误用比较函数设计,有序vector的效率几乎绝对优于关联容器。

22.该返回对象时则返回对象

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

pass-by-reference固然很好,但是实际应用时可能会发生一些很尴尬的场景,比如reference指向并不存在的对象。
具体而言,就是你试图用一个reference去绑定一个局部变量(local对象),这直接引发了雪崩。

可能你会在函数内部定义一个static对象,并用一个reference去绑定它,最终返回一个指向static对象的reference。在多线程下这种操作极易导致安全问题,并且它还有更致命的错误。举例而言:

1
2
3
4
5
6
const Rational& operator*(const Rational& lhs,
const Rational& rhs){
static Rational res;
res = ...;//乘法操作
return res;
}

当我们试图去使用operator时:
1
2
3
4
5
6
7
Rational a,b,c,d;
if(a*b)==(c*d){
...
}
else{
...
}

事实上if判定永远是true,因为​reference是static对象的别名,而那个对象和自己比对必然是相等的(虽然两次operator
的确各自改变了它的值,但最终仅返回了最终修改后的对象的reference)

固执的人可能会试图去使用static array.. 这比使用static对象还要令人发指。首先无法判定array的大小,小了不够用,多了又浪费。而且n太大了还会造成效率低下(执行了n次构造与析构)。这个愚蠢的想法无法优化,就算把array换成vector也不会改善情况

因此,不如就坦然地返回一个新对象,值得注意的是这里可以配合返回值优化使构造与析构的成本降到最低:(返回值优化详见 More Effective C++ 20)

1
2
3
inline const Rational operator*(const Rational& lhs,const Rational& rhs){
return Rational(...);//返回值优化
}

21.禁止直接修改set或mutiset的key

Posted on 2018-04-12 | In Effective STL

前言



正如所有标准关联容器,set和multiset保证元素有序,而且容器的行为之所以能够正确也是建立在内部元素有序的基础之上。如果你修改了关联容器里的元素的key,那么元素的有序可能得不到保证。


修改map的key

</br>
本节标题只提到了set与mutiset,因此有读者会发生疑问:难道map或mutimap的key就可以改变吗?实则不然:

1
2
3
4
5
6
map<int, string> m;
...
m.begin()->first = 10; //error
multimap<int, string> mm;
...
mm.begin()->first = 20; //error

map的key根本无法改变,任何试图改变其key的操作都无法通过编译,这也就是本节不涉及map的原因。
之所以map的key无法改变,那是因为map<K,V>或者multimap<K,V>类型的对象中元素的类型是pair<const K,V>,const保证了其key不可赋值。


为何set的key不能设为const

</br>
对于set<T>或multiset<T>类型的对象来说,储存在容器里的元素类型只不过是T,并非constT。因此,set或multiset里的元素可能在你想要的任何时候改变。那这个时候问题来了,为何set内的元素不能设置为const呢?

问题实例

假设存在一个关于雇员的class:

1
2
3
4
5
6
7
8
9
10
class Employee {
public:
...
const string& name() const;
void setName(const string& name);
const string& getTitle() const;
void setTitle(string& title);
int idNumber() const;
...
};

假定雇员ID唯一,建立一个内部元素为Employee对象的set,显然应该以ID来排序set:
1
2
3
4
5
6
7
8
struct IDNumberLess:
public binary_function<Employee, Employee, bool> {
bool operator()(const Employees lhs,const Employee& rhs) const{
return lhs.idNumber() < rhs.idNumber();
}
};
typedef set<Employee, IDNumberLess> EmpIDSet;
EmpIDSet se;

在此实例中,ID是set的key,其他东西自然可以任意更改:
1
2
3
4
5
Employee selectedID;
auto i = se.find(selectedID);
if (i != se.end()){
i->setTitle("Corporate Deity");//更换头衔
}

我们可以自由地改变set中对象的某一部分成员,那说明set内部的元素并没有用const修饰,那也就是说,set内部的一切都可以改变。改变set的key是存在巨大风险的,但编译器允许这种行为。

(Effective STL的作者接下来花了大量的篇幅讲述映射,我认为其内容并非一定需要了解)


如何安全地改变set内部的元素

</br>
想要安全地更改set内部的元素,必须按照以下流程执行

  1. 定位到需要修改的元素
  2. 拷贝并修改
  3. 从set中删除原元素
  4. 插入新元素
1
2
3
4
5
6
7
8
9
10
EmpIDSet se; 
Employee selectedID;
...
auto i =se.find(selectedID);
if(i!=se.end()){
Employee e(*i); //copy
se.erase(i++);//自增保证迭代器有效
e.setTitle("Corporate Deity");//修改副本
se.insert(i, e);//在原先位置插入新值
}

笔者认为在关联容器内部指定位置插入的做法不足取,但本节也仅仅只是权宜之举。对于set,记住不要试图去修改key就好了。

21.以pass-by-refernce-to-const代替pass-by-value

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

前言

</br>
默认情况下c++以by value方式传递对象至函数。除非你另外指定,否则函数形参都是实参的副本,返回值也是一个副本。这些副本都由对象的copy构造函数生成,并同时需要析构。反复的构造与析构造就了大量的开销。


以pass-by-refernce-to-const代替pass-by-value的优势

效率

pass-by-refernce-to-const效率极高,因为没有任何构造、析构函数被调用。使用const则使用户不必担心传入的对象遭到任何更改。

避免slicing

当一个derived class对象以by value的方式传递给base class对象,base class的构造函数会被调用,而derived class的特性却被切割,导致最终得到的就是一个base class对象。
pass-by-refernce-to-const 不会导致上述问题,这是由于动态绑定的原因。


内置类型与pass-by—reference

</br>
在编译器的底层实现中,reference往往以指针的形式表现出来,因此pass-by-refernce手法实际上传递的是一个指针。如果你有一个对象是内置类型,那可能用pass-by-value的效率更高一些,stl的迭代器和函数对象也是pass-by-value的效率更高。


小型对象与pass-by-reference

</br>
有读者误以为内置类型使用pass-by_value效率更高,就应该对所有的小型对象使用pass-by-value.这种思想是错误的,对象小并不意味着copy构造函数不昂贵(比如复制该对象需要复制其所有指向的东西,详情可见Effective STL 15)。就算它真的不昂贵,你也不能保证它以后还是不昂贵(未来时态编程 More Effective C++ 32)�,甚至有的时候不同的编译器都会改变type的大小。


总结

  1. 尽量以pass-by-refernce-to-const代替pass-by-value,前者是高效且正确的代名词。
  2. 规则1并不适用于内置类型,以及stl的迭代器与函数对象。

21.令比较函数在等值情况下返回false(严格弱序化)

Posted on 2018-04-12 | In Effective STL

set崩坏惨案

</br>
假设存在一个set:

1
2
3
set<int,less_equal<int> > s;//以<=排序
s.insert(10);
s.insert(10);//再次试图插入

显然我们在调用第二次insert之前,set必须判断10在不在里面。我们把首先插入的10记作10A,将后来想要插入的10记作10B.
set首先遍历自己找出在哪儿适合插入10B,并且最终比较10B与10A是否相同(基于等价的相同)。前文已经描述过,关联容器在判断相同时使用的是如下表达式:
1
!c.key_comp()(x, y) && !c.key_comp()(y, x);

那么针对本例中的set,有相同判断式(此式与比较函数并不相同,不要混淆)如下:
1
!(10A <= 10B) && !(10B <= 10A)

显然false&&false是false,所以set认为10A与10B不相同,于是set里面有了两个10.(夭寿啦)。通过使用less_equal作为比较器,我们成功地破坏了set。(在实测中,编译器会在运行期报错,提示比较器无效)
上述实例中导致相同判断式失效最关键的原因在于比较函数对两个等值对象返回了true。
在关联容器中必须牢记,对于两个相等的值,比较函数必须返回false.


实例分析

</br>
我们之前写过对指针的升序版本比较函数,如果我们现在需要一个降序版本(不用说,把之前的拿出来改一改):

1
2
3
4
5
6
struct StringPtrGreater://无效比较器
public binary_function<const string*,const string*, bool> {
bool operator()(const string *ps1, const string *ps2) const{
return !(*ps1 < *ps2);对原有排序原则取反 <变成了>=
}
};

该比较器是无效的,因为它对相等的值返回true(基于>=)真正的比较器如下:

1
2
3
4
5
6
struct StringPtrGreater://无效比较器
public binary_function<const string*,const string*, bool> {
bool operator()(const string *ps1, const string *ps2) const{
return *ps1 > *ps2;
}
};

比较函数对等值对象必须返回false的原因

</br>
比较函数的返回值表明的是在此函数定义的排序方式下,一个值是否大于另一个。相等的值绝不该一个大于另一个,所以比较函数总应该对相等的值返回false。


mutiset与mutimap同样适用的原因

</br>
有人会觉得该条款只对set与map有效,muti则不在此列,但实际上并非如此

1
2
3
multiset<int, less_equal<int> > s;
s.insert(10);
s.insert(10);

如果我们对s调用equal_range成员函数(标注所有与输入元素等值的元素的范围),但由于比较函数认为10A与10B并不相同,所以它们无法同时出现在范围内。


总结

</br>
从技术上说,比较函数应该严格遵守弱序化,其体现之一就是确保两个相等的值比较时返回false.

20.设计class犹如设计type

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

设计良好的class如同内置类型(type)一样,接口自然,目的明确。


在设计class之前,设计者必须扪心自问:

  • 新type的对象应该如何被创建和销毁?
    该问题的回答决定了构造函数、析构函数、内存分配和释放函数

  • 对象的初始化和赋值有何区别?
    该问题的回答决定了构造函数与赋值操作符的的行为。尤其需要注意的是,“初始化”、“赋值”是完全不同的概念,它们调用了不同的函数。

  • 新type的对象如果被pass by value,意味着什么?
    具体实现方式可参考Effective C++ 14;

  • 新type的合法值是什么?
    对于成员变量而言,可能仅有某些数值集有效。这些数值集决定了class的约束条件,同时也决定了setting函数(构造、赋值等)必须进行的错误检查,同时,它也影响函数抛出的异常。

  • 新type需要配合某个继承体系吗?
    如果当前type继承自某些class,那你必然受到了那些class设计的束缚,尤其是virtual或者non-virtual成员函数的影响。(详见Effective C++ 34/36/37/38)
    如果你允许别的类继承该type,那请谨慎考虑析构函数是否需要virtual。(Effective C++ 8)

  • 新type需要什么样的转换?
    如果你允许T1被隐式转换为T2,就必须在T1内写一个类型转换函数,或者为T2写一个non-explicit-one-argument(可被单一实参调用)的构造函数。(More Effective C++ 5)
    如果你只允许explicit构造函数存在,就必须撰写显式类型转换函数,并且禁止定义类型转换操作符或者non-explicit-one-argument构造函数。

  • 新type需要哪些操作符和成员函数?
    该问题的回答决定了class中需要声明的函数,其中有一些是成员函数,有些则不应该是。(Effective C++ 24,25,47)

  • 新type应当明确禁止哪些函数?
    该问题的回答决定了class中哪些函数需要声明为没有定义的private成员函数。(Effective C++ 6)

  • 谁会试图使用新type的成员?
    该问题的回答决定了成员函数的可见性,并且也决定了friend。

  • type的一般化程度如何?
    如果一般化的程度足够高,或许我们不应该定义一个class,而是应该定义一个class template。

  • 是否真的需要一个新type?
    如果你只是想增加了一个derived class为原有的class添加功能,先试试看能不能用一些non-member函数或者templates解决。


每一次写下class之前请默读本章所有问题。

20.为含有指针的关联容器指定比较类型

Posted on 2018-04-12 | In Effective STL

问题实例

</br>
假设存在一个元素类型为string 的set,其内容如下:

1
2
3
4
5
6
set<string*> ssp; //ssp = “set of string ptrs”
ssp.insert(new string("Anteater"));
ssp.insert(new string("Wombat"));
ssp.insert(new string("Lemur"));
ssp.insert(new string("Penguin"));
//以为set内部元素已然按照字典序排序

我们试图去遍历并输出它:
1
2
for (auto i = ssp.cbegin();i != ssp.cend();++i)
cout << *i << endl; //以为会获得按照字典序的string对象

但实际上输出的只是一堆地址,并不是string对象。
如果你使用的是算法而非显式循环:
1
copy(ssp.begin(), ssp.end(),ostream_iterator<string>(cout, "\n"));

你将更早地得到错误信息,上述代码无法编译,ssp内部存放的是string\
而非string,所以和ostream_iterator的打印类型不匹配。这也从侧面反映了算法至少在正确性上确实优于循环。(Effective STL 43)
就算我们在显式循环中使用**i,最后的输出也会不尽人意,你会发现输出的字符串并没有像你想的那样按照首字母排序,而是按照string* 的大小进行排序。
原因在于set使用了默认的排序器,ssp的声明其实等价于
1
2
set<string*> ssp;
set<string*, less<string*> > ssp;


自定义比较类型

</br>
如果我们试图以指针所指向的对象为排序对象,那么比较器应该改为自定义的比较仿函数类,如下所示:

1
2
3
4
5
6
struct StringPtrLess:
public binary_function<const string*, const string*, bool> {//该基类详见Effective STL 40
bool operator()(const string *ps1, const string *ps2) const{
return *ps1 < *ps2;
}
};

此时,可以声明ssp为
1
2
typedef set<string*, StringPtrLess> StringPtrSet;
StringPtrSet ssp;//按照字典序排序内部指针指向对象的set

这个时候再使用显式循环即可正确完成任务。

如果你想使用算法输出string对象,那必须对一个元素执行解引用,写一个谓词或者lambda配合for_each算法即可:

1
2
3
4
5
6
7
//谓词版本
void print(const string *ps){
cout << *ps << endl;
}
for_each(ssp.begin(), ssp.end(), print);
//lambda版本
for_each(ssp.begin(), ssp.end(),[](const string *ps){cout << *ps << endl;});

当然,你也可以自定义解引用仿函数类,然后配合transform与ostream_iterator:
1
2
3
4
5
6
7
struct Dereference {//传入T*,返回const T&
template <typename T>
const T& operator()(const T *ptr) const{
return *ptr;
}
};
transform(ssp.begin(), ssp.end(),ostream_iterator<string>(cout, "\n"),Dereference());


为何是比较类型而非比较函数

</br>
也许你会好奇为什么传递给set的永远是一个class而非某个函数,因此有人会试图直接使用函数作为参数声明set:

1
2
3
4
bool stringPtrLess(const string* ps1,const string* ps2){ 
return *ps1 < *ps2;
}
set<string*, stringPtrLess> ssp;//无法编译

原因在于set模板的三个参数均为class,而函数不需要类型。set不需要一个函数,它需要的是能在内部用实例化建立函数的一种类型。


通用解决方案

</br>
我们可以发现,在大多数情况下,比较类型只是解引用指针并比较所指向的对象。鉴于这种情况,我们完全可以定义一个通用仿函数模板:

1
2
3
4
5
6
struct DereferenceLess {
template <typename PtrType>
bool operator()(PtrType pT1,PtrType pT2) const {//这里采用了值传递,因为指针等内置类型值传递效率更高
return *pT1 < *pT2;
}
};

声明式如下:
1
set<string*, DereferenceLess> ssp;


总结

</br>
本节虽以指针为名,但实际上使用范围是内部元素表现为指针的容器:如元素是智能指针、迭代器等。在这些容器中务必自定义比较类型。

19.接口设计原则:易于使用,不易误用

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

前言

</br>
作为接口设计者,我们应该明确客户可能并不了解实现,也就是说,客户会大概率地不按期望方式使用接口。我们应当以下面的句子作为目标时刻鞭策自己:

  1. 如果客户使用了某个接口但却不能得到预期行为,该代码不应该被正确编译
  2. 如果某段代码通过了编译,它的行为必然是客户所期望的

问题实例

</br>
假定我们正在为一个表现日期的class设计构造函数:

1
2
3
4
5
class Date{
public:
Date(int month,int day,int year);
...
}

客户至少可能会针对这个看似不错的接口犯下两个错误:

  1. 以错误的次序传递参数
    1
    Date d(11,4,2018);
  2. 传递无效的日期
    1
    Date d(2,30,2018);

解决方案

导入新类型以预防

建立wrapper types以区分年月日

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;
};
class Date{
public:
Date(const Month& m,const Day& d,const Year &y);
...
};

如此一来,只有正确的类型才能导致正确的初始化。

限制wrapper types的取值

举例而言,Months的个数应当只有12个,那么使用enum来表征Month看起来是合理的。但是,enum不具备类型安全性(总可以转为int 详见Effective C++ 3),因此比较安全的写法是预先定义所有有效的Months:

1
2
3
4
5
6
7
8
9
class Month{
public:
static Month Jan() {return Month(1);}
static Month Feb() {return Month(2);}
.......
private:
explicit Month(int m);//禁止用户构造
}
Date d(Month::Mar(),Day(30),Year(1995))

值得注意的是这里是以函数替换对象,具体原因见Effective C++ 5 non—local static对象的初始化顺序一节。

限定类型可以执行的任务

最常见的限制是加上const。
比如我们可以对operator*的返回值加上const,从而让诸如if(a*b=c)这样的东西失去了意义。(用户再也不用担心把==写作=了)


令自定义类型的行为与内置类型保持一致

</br>
该原则的本质是提供行为一致的接口​。一致性是快速学习与开发的前提。(写算法时也应该注意语义一致)


接口不应当要求用户必须做某事

</br>
任何接口如果要求客户必须记得做某件事,那么说明它必然存在“不正确使用”的可能,因为墨菲定律,因为用户的操作无法由开发者决定。

RAII的进一步优化(factory模式)

factory函数总是返回指向一个动态分配的object的指针,仍以Investment举例:

1
Investment* createInvestment();

为了避免资源泄漏,factory函数返回的指针最终必须要被删除。这给了用户两个错误机会:

  1. 忘了delete
  2. 重复delete

我们采用RAII避免了资源泄露,但前提是用户必须了解和记得使用智能指针来管理返回的指针。因此,factory函数可以先发制人,直接就返回一个智能指针:

1
shared_ptr<Investment> createInvestment();

shared_ptr默认的析构行为是delete raw point,这可能与客户所需要的删除操作(deleter)不符,因此我们干脆初始化一个指向nullptr并且删除器是deleter的shared_ptr,最终,factory函数如下:
1
2
3
4
5
shared_ptr<Investment> createInvestment(){
shared_ptr<Investment> retVal(nullptr,deleter);
retval=...;//指向需要管理的资源
return retval;
}

以上是原始指针未能确定的情况,实际上把原始指针直接传给shared_ptr构造函数的话效果更佳。


总结

  1. 接口应当易于使用,不易误用。
  2. 促进正确使用的方法包括语义一致性,以及行为兼容性。
  3. 阻止误用的办法包括建立新外覆类型,限制类型操作,束缚对象值,以及改进RAII。
  4. shared_ptr支持定制删除器,这样可以防范dll问题(跨DLL删除,自动解除互斥锁)。

19.相等与等价的区别

Posted on 2018-04-11 | In Effective STL

前言

</br>
在STL中,对两个对象进行比较,并且判定它们是否具有相同的值,这种操作十分常见。举例而言,find算法可以定位区间中第一个具有某个特定值的元素,这种操作必须基于对象的比较。set的insert成员函数在执行插入前也必须明确该值是否已经存在于容器内部,这同样基于比较。


相等与等价

</br>
上文所说的find算法与insert成员函数都需要判定两个值是否相同,但其实现则完全不同。
find对“相同”的定义是相等,基于operator==。
set::insert对“相同”的定义是等价,通常基于operator<。

相等

相等的概念基于operator==。如果表达式“x == y”返回true,我们认为x与y具有相等的值。这种相等是定义性的,只和operator==的返回值有关。也就是说,只要operator==撰写者愿意,我们完全可以判定猫和狗相等。

等价

等价基于一个有序区间中对象值的相对位置。也就是说,等价仅在关联容器内部排序时存在意义。等价的定义是,如果对象x与对象y在关联容器c中的排序顺序并不分先后,则它们等价。从逻辑角度而言,基于operator<的等价有这样的形式:

1
!(x<y) && !(y<x)

一般情况下关联容器的比较函数不是operator<或 less,是用户自定义的判断式。每个关联容器通过key_comp成员函数来访问排序判断式,因此真正意义上的等价有形式如下:
1
!c.key_comp()(x, y) && !c.key_comp()(y, x) //key_comp返回一个函数(或函数对象)


问题实例

建立case-insensitive string set

假设需要建立一个忽略大小写的set<string>,显然,我们需要自行建立一个比较函数,该函数比较string对象的同时忽略大小写。set在声明时需要的是比较函数的类型,并非真的函数,因此我们构建了一个仿函数类(仿函数详见Effective STL·仿函数篇),并在operator()调用了忽略大小写的函数:

1
2
3
4
5
6
struct CIStringCompare: public binary_function<string, string, bool> {
//binary_function 详见Effective STL 40
bool operator()(const string& lhs,const string& rhs) const{
return ciStringCompare(lhs, rhs);//具体实现见Effective STL 35
}
}

该set的建立和使用如下:
1
2
3
set&lt;string, CIStringCompare> ciss;//"case-insensitive string set"
ciss.insert("ABC"); // 一个新元素添加到set中
ciss.insert("abc"); // 没有新元素添加到set中

find成员函数与find算法的不同

我们在新建立的容器中查找刚才加入的”abc”(大小写被忽略),find成员函数与find算法返回的值却并不相同:

1
2
if (ciss.find("abc") != ciss.end())... //true
if (find(ciss.begin(), ciss.end(),"abc") != ciss.end())... //false

这是十分自然的,因为find成员函数判定”abc”等价于”ABC”,而find算法判定二者并不相等。


关联容器为什么需要基于等价来判定相同?

</br>
标准关联容器默认有序,所以STL关联容器必须存在一个保证有序的比较函数less。如果以相等来决定二者是否具有相同的值,则关联容器除了比较器之外还需要定义一个判断相等的比较函数。这两个函数可能会引发冲突。
举例而言,假定存在一个类似set的STL容器叫做set2CF(set with two comparison functions)。第一个比较函负责定set内部元素的排序,第二个用来判定两个元素是否相同。

1
set2CF<string, CIStringCompare, equal_to<string> > s;

排序函数判定字符串时不考虑大小写,而等价函数则认为,两个字符串对应字符完全相同时它们相同。(此时存在两个比较函数的逻辑混乱)当执行如下操作时:
1
2
s.insert("ABC");
s.insert("abc");

从第一比较器的角度而言,二者是等价的,第二个不会被插入。第二比较器则认为二者并不相同。
那如果遵循第二比较器,把两个string都放入set,那么它们又应该处于什么顺序呢?如果随意的插入,则等于放弃了顺序遍历set的能力。(set内无法保证有序)

综上,在关联容器中使用等价作为相同判定时具备先天优势。(关联容器必须有序,而等价的定义就和顺序相关)

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