关于相等和不等
template<class T, class U> inline bool operator==(shared_ptr<T> const & a, shared_ptr<U> const & b)
{
return a.get() == b.get();
}
template<class T, class U> inline bool operator!=(shared_ptr<T> const & a, shared_ptr<U> const & b)
{
return a.get() != b.get();
}
由此可以看出,实际上它们就是简单的比较内部原始指针是否相等。因此可以得出这样的判断,对于使用==来比较的时候,以下情况下a和b会相等:
1.a、b都为空或者是都包含空指针,或者是其中之一。比如说如果a为空,b为空指针,那么也是相等的。
2.如果a和b存在着拷贝构造或者是赋值关系,那么它们也是相等的。
同时我们也应该注意到相等判断只与内部对象的地址有关系和内部对象的值没有关系,比如说:
shared_ptr<int> sp1 (new int (3));
shared_ptr<int> sp2 (new int (3));
由于sp1和sp2内部的指针肯定不相同,因此sp1和sp2肯定不相等。
而不等于比较简单,就是!(a==b)。
对于STL的关联容器,它们对于是否存在的判断标准是等价而不是相等。而等价的定义是如此:
template <class Ty1, class Ty2>
inline bool equiv(shared_ptr<Ty1> left,
shared_ptr<Ty2> right)
{
return !(left < right) && !(right < left);
}
对于shared_ptr来说,当且仅当如下情况时,这个函数才返回true,也就是等价关系成立:
1. left和right都为空。即:left和right都使用默认构造函数构造。
2. left和right控制相同的受控源。即:left和right存在着某一方从另外一方拷贝构造或者赋值的关系。
对于等价判断实际上我们选择的是shared_ptr的重载<,通过查看实现源码可以发现最终发生比较关系的是shared_count的一个成员数据指针pi_:sp_counted_base * pi_;
friend inline bool operator<(shared_count const & a, shared_count const & b)
{
return std::less<sp_counted_base *>()( a.pi_, b.pi_ );
}
因此,重载<只有当pi_不相同的时候才有可能返回true。上面的语句通常情况下等于: return a.pi_ < b.pi_。通过参考上面的判别式!(left < right) && !(right < left)我们可以知道,除非a.pi_和b.pi_相同,否则判别式!(left < right) && !(right < left)永远不可能为true。
下面就可以把重点集中在什么时候会导致a.pi_与b.pi_相等了。
通过查看源码我们可以发现当且仅当shared_count类对象使用默认构造的时候pi_才会被初始化为0,其它有参数的情况都会发生内存分配,返回值保存在pi_当中,先考察下面这几个构造函数:
shared_count()
template<class Y> explicit shared_count( Y * p )
template<class P, class D> shared_count( P p, D d )
template<class P, class D, class A> shared_count( P p, D d, A a )
template<class Y> explicit shared_count( std::auto_ptr<Y> & r )
这其中只有shared_count没有对pi_进行内存分配,其它都分配了,而它们的参数均是直接由shared_ptr直接传递进来的。而默认构造shared_ptr的时候,shared_count亦使用默认构造。下面是shared_ptr的数据成员:
T * px; // contained pointer
boost::detail::shared_count pn; // reference counter
下面的代码证明了我所说的构造方式:
shared_ptr(): px(0), pn() // never throws in 1.30+
{
}
template<class Y>
explicit shared_ptr( Y * p ): px( p ), pn( p ) // Y must be complete
{
boost::detail::sp_enable_shared_from_this( pn, p, p );
}
//
// Requirements: D's copy constructor must not throw
//
// shared_ptr will release p by calling d(p)
//
template<class Y, class D> shared_ptr(Y * p, D d): px(p), pn(p, d)
{
boost::detail::sp_enable_shared_from_this( pn, p, p );
}
// As above, but with allocator. A's copy constructor shall not throw.
template<class Y, class D, class A> shared_ptr( Y * p, D d, A a ): px( p ), pn( p, d, a )
{
boost::detail::sp_enable_shared_from_this( pn, p, p );
}
上面这些代码证明了一点,在没有发生拷贝构造和赋值的前提下,只有当使用默认构造的shared_ptr才等价,其它情况都不等价,哪怕T * px;的这个px相等,因为它跟等价比较没有任何关系。同时通过源码分析我们也可以发现一个奇怪的现象,即:
如果使用的是默认构造,那么即便是shared_ptr的类型不同,那么它们也会等价。而对于相等判断来说,这会导致一个编译错误。
当然,你明白原理了就不奇怪了,因为你所比较的对象是类型无关的。下面的代码将证明我所说的:
#include <iostream>
#include <boost/tr1/memory.hpp>
using namespace std;
using namespace std::tr1;
#define PrintExp( exp ) \
cout<< boolalpha << "( " << # exp << " ) = " << (exp) << endl
template <class Ty1, class Ty2>
inline bool equiv(shared_ptr<Ty1> left,
shared_ptr<Ty2> right)
{
return !(left < right) && !(right < left);
}
int main()
{
shared_ptr<int> sp1( new int(100) );
shared_ptr<int> sp2;
PrintExp( equiv( sp1, sp2 ) );
shared_ptr<double> sp3;
PrintExp( equiv( sp2, sp3 ) );
shared_ptr<int> sp4;
PrintExp( equiv( sp2, sp4 ) );
shared_ptr<int> sp5( (int*)NULL );
PrintExp( equiv( sp4, sp5 ) );
return 0;
}
等价还有另外一种情况,即通过拷贝构造或者赋值,下面的代码清楚的说明了为什么这些情况下,我们的pi_会相等最终导致判别式!(left < right) && !(right < left)为真:
shared_count(shared_count const & r): pi_(r.pi_) // nothrow
#if defined(BOOST_SP_ENABLE_DEBUG_HOOKS)
, id_(shared_count_id)
#endif
{
if( pi_ != 0 ) pi_->add_ref_copy();
}
// throws bad_weak_ptr when r.use_count() == 0
explicit shared_count(weak_count const & r);
// constructs an empty *this when r.use_count() == 0
shared_count( weak_count const & r, sp_nothrow_tag );
shared_count & operator= (shared_count const & r) // nothrow
{
sp_counted_base * tmp = r.pi_;
if( tmp != pi_ )
{
if( tmp != 0 ) tmp->add_ref_copy();
if( pi_ != 0 ) pi_->release();
pi_ = tmp;
} // 如果这里还有个else的话,那么就意味着pi_=tmp,因为tmp=r.pi_,则pi_=r.pi_。
// 那么pi_=tmp;这个赋值动作有
// 没有又有什么关系呢?
return *this;
}
到这里我想我们对此应该形成了一个比较清楚的概念了吧,呵呵。由于!(left < right) && !(right < left)的支持,使得shared_ptr可以作为容器的Key,比如说set、map等。另外补充一下就是weak_ptr也有类似的行为。
下面再附上一篇,这选自《Effective STL》的条款19,对我们理解这些很有帮助。
条款19:了解相等和等价的区别
STL充满了比较对象是否有同样的值。比如,当你用find来定位区间中第一个有特定值的对象的位置,find必须可以比较两个对象,看看一个的值是否与另一个相等。同样,当你尝试向set中插入一个新元素时,set::insert必须可以判断那个元素的值是否已经在set中了。
find算法和set的insert成员函数是很多必须判断两个值是否相同的函数的代表。但它们以不同的方式完成,find对“相同”的定义是相等,基于operator==。set::insert对“相同”的定义是等价,通常基于operator<。因为有定义不同,所以有可能一个定义规定了两个对象有相同的值而另一个定义判定它们没有。结果,如果你想有效使用STL,那么你必须明白相等和等价的区别。
操作上来说,相等的概念是基于operator==的。如果表达式“x == y”返回true,x和y有相等的值,否则它们没有。这很直截了当,但要牢牢记住,因为x和y有相等的值并不意味着所有它们的成员有相等的值。比如,我们可能有一个内部记录了最后一次访问的Widget类。
class Widget {
public:
...
private:
TimeStamp lastAccessed;
...
};
我们可以有一个用于Widget的忽略这个域的operator==:
bool operator==(const Widget& lhs, const Widget& rhs) {
// 忽略lastAccessed域的代码
}
在这里,两个Widget即使它们的lastAccessed域不同也可以有相等的值。
等价是基于在一个有序区间中对象值的相对位置。等价一般在每种标准关联容器(比如,set、multiset、map和multimap)的一部分——排序顺序方面有意义。两个对象x和y如果在关联容器c的排序顺序中没有哪个排在另一个之前,那么它们关于c使用的排序顺序有等价的值。这听起来很复杂,但实际上,它不。考虑一下,举一个例子,一个set<Widget> s。两个Widget w1和w2,如果在s的排序顺序中没有哪个在另一个之前,那么关于s它们有等价的值。set<Widget>的默认比较函数是less<Widget>,而默认的less<Widget>简单地对Widget调用operator<,所以w1和w2关于operator<有等价的值如果下面表达式为真:
!(w1 < w2) // w1 < w2时它非真
&& // 而且
!(w2<w1) // w2 < w1时它非真
这个有意义:两个值如果没有哪个在另一个之前(关于某个排序标准),那么它们等价(按照那个标准)。
在一般情况下,用于关联容器的比较函数不是operator<或甚至less,它是用户定义的判断式。(关于判断式的更多信息参见条款39。)每个标准关联容器通过它的key_comp成员函数来访问排序判断式,所以如果下式求值为真,两个对象x和y关于一个关联容器c的排序标准有等价的值:
!c.key_comp()(x, y) && !c.key_comp()(y, x) // 在c的排序顺序中
// 如果x在y之前它非真,
// 同时在c的排序顺序中
// 如果y在x之前它非真
表达式!c.key_comp()(x, y)看起来很丑陋,但一旦你知道c.key_comp()返回一个函数(或一个函数对象),丑陋就消散了。!c.key_comp()(x, y)只不过是调用key_comp返回的函数(或函数对象),并把x和y作为实参。然后对结果取反,c.key_comp()(x, y)仅当在c的排序顺序中x在y之前时返回真,所以!c.key_comp()(x, y)仅当在c的排序顺序中x不在y之前时为真。
要完全领会相等和等价的含义,考虑一个忽略大小写的set<string>,也就是set的比较函数忽略字符串中字符大小写的set<string>。这样的比较函数会认为“STL”和“stL”是等价的。条款35演示了怎么实现一个函数,ciStringCompare,它进行了忽略大小写比较,但set要一个比较函数的类型,不是真的函数。要天平这个鸿沟,我们写一个operator()调用了ciStringCompare的仿函数类:
struct CIStringCompare: // 用于忽略大小写
public // 字符串比较的类;
binary_function<string, string, bool> { // 关于这个基类的信息
// 参见条款40
bool operator()(const string& lhs,
const string& rhs) const
{
return ciStringCompare(lhs, rhs); // 关于ciStringCompare
} // 是怎么实现的参见条款35
}
给定CIStringCompare,要建立一个忽略大小写的set<string>就很简单了:
set<string, CIStringCompare> ciss; // ciss = “case-insensitive
// string set”
如果我们向这个set中插入“Persephone”和“persephone”,只有第一个字符串加入了,因为第二个等价于第一个:
ciss.insert("Persephone"); // 一个新元素添加到set中
ciss.insert("persephone"); // 没有新元素添加到set中如果我们现在使用set的find成员函数搜索字符串“persephone”,搜索会成功,
if (ciss.find("persephone") != ciss.end())... // 这个测试会成功但如果我们用非成员的find算法,搜索会失败:
if (find(ciss.begin(), ciss.end(),
"persephone") != ciss.end())... // 这个测试会失败那是因为“persephone”等价于“Persephone”(关于比较仿函数CIStringCompare),但不等于它(因为string("persephone") != string("Persephone"))。这个例子演示了为什么你应该跟随条款44的建议优先选择成员函数(就像set::find)而不是非成员兄弟(就像find)的一个理由。
你可能会奇怪为什么标准关联容器是基于等价而不是相等。毕竟,大多数程序员对相等有感觉而缺乏等价的感觉。(如果不是这样,那就不需要本条款了。)答案乍看起来很简单,但你看得越近,就会发现越多问题。
标准关联容器保持有序,所以每个容器必须有一个定义了怎么保持东西有序的比较函数(默认是less)。等价是根据这个比较函数定义的,所以标准关联容器的用户只需要为他们要使用的任意容器指定一个比较函数(决定排序顺序的那个)。如果关联容器使用相等来决定两个对象是否有相同的值,那么每个关联容器就需要,除了它用于排序的比较函数,还需要一个用于判断两个值是否相等的比较函数。(默认的,这个比较函数大概应该是equal_to,但有趣的是equal_to从没有在STL中用做默认比较函数。当在STL中需要相等时,习惯是简单地直接调用operator==。比如,这是非成员find算法所作的。)
让我们假设我们有一个类似set的STL容器叫做set2CF,“set with two comparison functions”。第一个比较函数用来决定set的排序顺序,第二个用来决定是否两个对象有相同的值。现在考虑这个set2CF:
set2CF<string, CIStringCompare, equal_to<string> > s; 在这里,s内部排序它的字符串时不考虑大小写,等价标准直觉上是这样:如果两个字符串中一个等于另一个,那么它们有相同的值。让我们向s中插入哈迪斯强娶的新娘(Persephone)的两个拼写:
s.insert("Persephone");
s.insert("persephone");
着该怎么办?如果我们说"Persephone" != "persephone"然后两个都插入s,它们应该是什么顺序?记住排序函数不能分别告诉它们。我们可以以任意顺序插入,因此放弃以确定的顺序遍历set内容的能力吗?(不能已确定的顺序遍历关联容器元素已经折磨着multiset和multimap了,因为标准没有规定等价的值(对于multiset)或键(对于multimap)的相对顺序。)或者我们坚持s的内容的一个确定顺序并忽略第二次插入的尝试(“persephone”的那个)? 如果我们那么做,这里会发生什么?
if (s.find("persephone") != s.end())... // 这个测试成功或失败?大概find使用了等价检查,但如果我们为了维护s中元素的一个确定顺序而忽略了第二个insert的调用,这个find会失败,即使“persephone”的插入由于它是一个重复的值的原则而被忽略!
总之,通过只使用一个比较函数并使用等价作为两个值“相等”的意义的仲裁者,标准关联容器避开了很多会由允许两个比较函数而引发的困难。一开始行为可能看起来有些奇怪(特别是当你发现成员和非成员find可能返回不同结果),但最后,它避免了会由在标准关联容器中混用相等和等价造成的混乱。
有趣的是,一旦你离开有序的关联容器的领域,情况就变了,相等对等价的问题会——已经——重临了。有两个基于散列表的非标准(但很常见)关联容器的一般设计。一个设计是基于相等,而另一个是基于等价。我鼓励你转到条款25去学更多这样的容器和设计以决定该用哪个。