19.5 explicit_cast(1)
现在我们知道了关于强制的更多东西,我们可以更仔细地考察16.4节提到的explicit_cast了。问题在于如何在避免隐式转换问题的前提下,提供我们自己的从Time类到std::tm和DATE的显式转换。当时我给出的第二个选择是使用explicit_cast组件。
- class Time
- {
- operator explicit_cast<tm> () const;
- operator explicit_cast<DATE> () const;
- };
既然我们刚刚学到用类模板来实现强制操作符,我们可以认为explicit_cast具有类似如下的实现:
程序清单19.5
- template <typename T>
- class explicit_cast
- {
- // 构造
- public:
- explicit_cast(T t)
- : m_t(t)
- {}
- // 转换
- public:
- operator T () const
- {
- return m_t;
- }
- // 成员
- private:
- T m_t;
- };
这对于基本类型和指针类型来说确实能够工作。但是,这是C++(www.cppentry.com),在C++(www.cppentry.com)中这种简单的东西永远也不可能成为最终解决方案。问题在于m_t是一个成员变量,explicit_cast的构造函数将它拷贝初始化为t。如果T是基本类型,这种拷贝通常是没问题的。事实上,对于基本类型和指针类型来说编译器很容易就能将这些额外的开销都优化掉 (甚至对于引用也是这样,不过有一些限制,见下文)。
对于基本类型来说,explicit_cast看起来非常完备。然而,对于用户自定义的类型来说又如何呢?如果T具有一个非平凡的构造函数,譬如一个包含有字符串元素的向量(vector),你可能就得付出一笔难以接受的拷贝开销。
- class Path
- {
- public:
- operator explicit_cast<string> () const;
- };
- void ProcessPath(Path const &path)
- {
- string s = explicit_cast<string>(path); // 多重拷贝!
- . . .
事实上,在上面这样的代码中,单个强制操作中可能隐含着几次拷贝。Borland、Digital Mars、GCC、Intel和Watcom都创建了3份拷贝;CodeWarrior和Visual C++(www.cppentry.com)(在不带Microsoft扩展的情况下)则创建4份拷贝;Visual C++(www.cppentry.com)(使用Microsoft扩展功能)甚至创建5份(而这还是在打开"最大化速度优化"选项的前提下编译的结果)。不用说,我们要避免这些开销。然而,由于只要是程序员都应该知道拷贝非平凡类型会引入开销,所以我们把它看成公认的按值返回(return-by-value)的副作用从而接受它。
另一个问题是在使用引用时出现的。因为语言不允许non-const引用绑定到临时变量。总的来说这是一个极其合理的规则,因为如果不加这个限制的话,人们就可以在代码中改变临时变量,这么一来,一头雾水的程序员就可能要竭力弄明白他们所作的改动到底怎么了,为什么它们随着临时变量一并"蒸发"了。Effective C++(www.cppentry.com)[Meye1998]中的条款30讨论了这一点,并且还很好地解释了为什么人们应该避免让类方法返回类成员变量的non-const引用。倘若explicit_cast提供向non-const引用转换的能力,情况就更糟糕了。因此我想说的是,即使这种情况下持有non-const引用的explicit_cast临时变量可能是有效的(因为explicit_cast临时变量只不过是简单地把它"有效地"获取的引用转手交出去而已),我们也不应该为这种用法不能通过编译而感到苦恼。
- class Path
- {
- public:
- operator explicit_cast<string &> (); // 危险!
- };
- void ProcessPath(Path &path)
- {
- string &s = explicit_cast<string&>(path); // 不合法,也不好!
- . . .
这样看来我们就只能往const引用转换了。实际上,上面代码中的强制没有任何理由不能工作,只不过是在有些编译器上不能编译而已。实际上该强制应该是能够通过编译的,但问题在于有些编译器会创建被引用类型的中间临时拷贝。具体说来,Intel 6.0、7.0和7.1以及Visual C++(www.cppentry.com) 6.0和7.0似乎都会创建额外的临时对象。而Borland、Digital Mars、CodeWarrior、GCC、Visual C++(www.cppentry.com)7.1以及Watcom的行为则跟我们期望的一样。 - class Path
- {
- public:
- operator explicit_cast<string const &>() const;
- };
- void ProcessPath(Path const &path)
- {
- // 可以工作,但这里可能会创建一个临时变量
- string const &s = explicit_cast<string const &>(path);
- . . .
总结一下:我们缺少一个语言特性,即"explicit_cast",尽管我们很少需要该特性,但它非常有用(我们将会在第六部分看到这方面的例子)。我们现在有一个组件,对于基本类型以及任何指针类型,它能够提供理想的行为,但对于引用类型来说则存在问题。它对于non-const引用不能工作,但是我们能够容忍这一点,因为它就算不违反语言规则,也是一项不明智且很少有用的能力。然而,该组件是有缺点的,它在一些编译器上会创建不必要的拷贝,而在另一些上则不会。代码在不同编译器之间的不一致的行为(即便是错在某些编译器自身 )对于质量合格的软件来说是不可接受的,因此我们需要对此做点什么。编写一个泛型组件并向用户宣称它能够且只应该被用在某些编译器上,而不能被用在其他编译器上,这种做法显然是不可接受的,因为在那些它不能像预期的那样工作的编译器上,它也能够通过编译而不会导致任何警告。人们完全有权拒绝使用它,而且或许还可能因此对你的其他作品退避三舍。
我们该做些什么呢?呃……我们得阻止explicit_cast被用在用户自定义类型的引用上,同时还必须允许它被用在基本类型的(const)引用上。有两种方式可以用来防止对该组件的不恰当使用:特化和约束。
第一种方式是同时使用局部特化和完全特化,如下:
程序清单19.6
- // 阻止用户将explicit_cast用在任何引用类型之上
- template <typename T>
- class explicit_cast<T &>
- {
- // 拷贝
- private:
- explicit_cast(T &);
- // 转换
- private:
- operator T & ();
- };
-
- // 显式地允许将它用在特定的基本类型身上
-
- template <>
- class explicit_cast<char const &>
- {
- // 构造
- public:
- explicit_cast(char const &t)
- : m_t(t)
- {}
- // 转换
- public:
- operator char const & () const
- {
- return m_t;
- }
- // 成员
- private:
- char const &m_t;
- };
-
- . . . // 对bool和wchar_t重复上面的代码
- . . . // 对signed/unsigned char重复上面的代码
- . . . // 对(unsigned) short/int/long/long long重复上面的代码
- . . . // 对float、double和long double重复上面的代码
-
- // 支持所有指针类型
- template <typename T>
- class explicit_cast<T *>
- {
- // 构造
- public:
- explicit_cast(T *t)
- : m_t(t)
- {}
- // 转换
- public:
- operator T * ()
- {
- return m_t;
- }
- // 成员
- private:
- T *m_t;
- };