条款42:确定less<T>表示operator<

优质
小牛编辑
127浏览
2023-12-01

条款42:确定less<T>表示operator<

正如所有了解零件(Widget)的人所知道的,Widget有重量和最高速度:

class Widget {
public:
	...
	size_t weight() const;
	size_t maxSpeed() const;
	...
};

此外众所周知的是给Widget排序的自然方法是按重量。用于Widget的operator<表现成这样:

bool operator<(const Widget& lhs, const Widget& rhs)
{
	return lhs.weight() < rhs.weight();
}

但是假设我们想建立一个按照最高速度排序Widget的multiset<Widget>。我们知道multiset<Widget>的默认比较函数是less<Widget>,而且我们知道默认的less<Widget>通过调用Widget的operator<来工作。鉴于这种情况,好像得到以最高速度排序的multiset<Widget>很明显一种方法是通过特化less<Widget>来切断less<Widget>和operator<之间的纽带,让它只关注Widget的最高速度:

template<>						// 这是一个std::less
struct std::less<Widget>:					// 的Widget的特化;
	public							// 也是非常坏的主意
	std::binary_function<Widget,
				Widget,			// 关于这个基类更多
				bool> {			// 的信息参见条款40
	bool operator()(const Widget& lhs, const Widget& rhs) const
	{
		return lhs.maxSpeed() < rhs.maxSpeed();
	}
};

这看起来既有欠考虑又确实有欠考虑,但你想到的理由可能不欠考虑。你是不是很奇怪它完全可以编译?很多程序员指出上述不只是一个模板的特化,而且是在特化std namespace中的模板。“std不应该是神圣的,为库的实现保留,而且超出了一般程序员可以达到的范围?”他们问,“编译器不该拒绝这个干预C++不朽的运转的尝试吗?”他们很奇怪。

通常,试图修改std里的组件确实是禁止的(而且这么做通常被认为是行为未定义的范畴),但是在一些情况下,修补是允许的。具体来说,程序员被允许用自定义类型特化std内的模板。特化std模板的选择几乎总是更为优先,但很少发生,这确实合理的。例如,智能指针类的作者经常想让他们的类在排序的时候行为表现得像内建指针,因此用于智能指针类型的std::less特化并不罕见。例如下面内容,是Boost库的shared_ptr的一部分,你可以在条款7和50中获悉智能指针:

namespace std {
	template<typename T>				// 这是一个用于boost::shared_ptr<T>
	struct less<boost::shared_ptr<T> >:			// 的std::less的特化
		public					// (boost是一个namespace)
		binary function<boost::shared_ptr<T>,
					boost::shared_ptr<T>,// 这是惯例的
					bool> {		// 基类(参见条款40)
		bool operator()(const boost::shared_ptr<T>& a,
					const boost::shared_ptr<T>& b) const
		{
			return less<T*>()(a.get(),b.get());	// shared_ptr::get返回
		}					// shared_ptr对象内的
							// 内建指针
	};
}

这不过分,当然也没有什么奇怪的,因为这个less的特化仅仅在排序上保证智能指针的行为与它们的内建兄弟相同。哎,我们试验性的用于Widget的less特化却比较奇怪。

C++程序员可以被原谅存在某些假设。例如,他们假设拷贝构造函数拷贝。(就像条款8证明的,没有依照这个约定可能导致惊人的行为。)他们假设对一个对象取地址就会产生一个指向那个物体的指针。(转向条款18去了解当这不为真时将发生什么。)他们假设像bind1st和not2这样的适配器可能应用于函数对象。(条款40解释了当不是这样时,事情是如何被破坏的。)他们假设operator+是加法(除了string,但是使用“+”表示字符串的连接已经有悠久的历史了),operator-是减法,operator==作比较。而且他们假设使用less等价于使用operator<。

operator<不仅是实现less的默认方式,它还是程序员希望less做的。让less做除operator<以外的事情是对程序员预期的无故破坏。它与所被称为“最小惊讶的原则”相反。它是冷淡的。它是低劣的。它是坏的。你不该那么做。

特别是当没有理由时。在STL中没有一个用到less的地方你不能指定一个不同的比较类型。回到我们原先以最高速度排序的multiset<Widget>的例子,我们要得到希望的结果所需的所有事情就是建立一个叫做除了less以外的几乎任何名字的仿函数类来对我们感兴趣的东西进行比较。嗨,这里有一个例子:

struct MaxSpeedCompare:
	public binary_function<Widget, Widget, bool> {
	bool operator()(const Widget& lhs, const Widget& rhs) const
	{
		return lhs.maxSpeed() < rhs.maxSpeed();
	}
};

要创造我们的multiset,我们使用MaxSpeedCompare作为比较类型,因此避免了默认比较类型的使用(当然也就是less<Widget>):

multiset<Widget, MaxSpeedCompare> widgets;

这条代码确切地说出了它的意思。它建立了一个Widget的multiset,按照仿函数类MaxSpeedCompare所定义方法排序。

对比这个:

multiset<Widget> widgets;

这个表示widgets是一个以默认方式排序的Widget的multiset。在技术上,那表示它使用了less<Widget>,但是实际上每人都要假设那真的意味着它是按operator<来排序。

不要通过把less的定义当儿戏来误导那些程序员。如果你使用less(明确或者隐含),保证它表示operator<。如果你想要使用一些其他标准排序对象,建立一个特殊的不叫做less的仿函数类。它真的很简单。