14.6.5 确保类型的大小相同(1)
前面我们讨论的机制中的任何一种,都不能提供一种理想或恰当的解决方案,以防止对派生类数组的不恰当使用。在考察真正的解决方案之前,我们应该先来考虑一下将数组算法应用到派生类型的情况,尽管这种举动是危险的,然而有时候确实有这个需要。我们考虑这样一个约束:被数组处理函数所使用的任何派生类型跟它们的基类型都占用同样大小的内存,因而在这种情境中使用它们是安全的。如果以上约束可以被强制实施的话,情况又当如何呢?如果它们(基类和派生类实例)具有相同大小的话,就不会存在"对象切割"问题(见第21章)了,同时因为一个派生类实例同时也是基类实例,所以在处理函数中将它们如此对待是完全可行的。
现在看来,如果派生类型跟基类型具有相同的大小,我们就可以允许创建派生类型的数组。这样一来,问题就变成了我们该如何确保这两个类型的大小是相同的。专业的代码审查者可能具有在代码审查时确认这一点的能力,不过也只能在有限的情况下。关键是在现实中有多种因素使得这种想法不切实际,这些因素包括模板派生(见第21章和第22章)、结构pack(见第13章和第14章)以及派生类空间开销(见12.4节)。甚至审查者在受到引导的情况下(这种情况比较罕见)进行代码审查也未必能够保证捕获所有的错误,而且这也只不过是他们海量验证工作[Glas2003]的一部分而已。
我们可以将断言置于代码中(见1.4节),但运行期断言可能得不到触发,比方说,代码路径覆盖不够全面, 或者是在发布版的测试中。 而编译期断言则好多了,但它给出的错误消息可读性较差,而且在特定的派生类中忽略它们可能并不会引起代码审查者的注意。一个更好的方式是使用约束(见1.2节)。约束是一段特殊的代码,通常为一个类模板,该类模板的作用是强制实施一个设计上的假定。此强制实施通常是以编译期错误的形式来呈现的,例如不能将某个类型转换为另一个类型。由于我们希望使类型具有相同的大小,所以该约束被命名为must_be_same_size(见1.2.5小节)。
现在,我们手上有了一个可用于侦测不恰当的派生类数组的工具,但是我们应该在哪里使用它呢?事实上,答案在std::vector解决方案中已经提出了:使用具有继承关系的不同的类型来参数化模板,将会导致实例化的结果是两个互不相干的类型。我们最终的解决方案是采用在14.4节中看到的array_proxy模板的改进版本。程序清单14.6展示了它的完整形式,引入了上面提到的约束以及一些额外的成员模板构造函数。
程序清单14.6
- template <typename T>
- class array_proxy
- {
- public:
- typedef T value_type;
- typedef array_proxy<T> class_type;
- typedef value_type *pointer;
- typedef value_type *const_pointer; // Non-const!
- typedef value_type &reference;
- typedef value_type &const_reference; // Non-const!
- typedef size_t size_type;
- // 构造函数
- public:
- template <size_t N>
- explicit array_proxy(T (&t)[N]) // 元素类型为T的数组
- : m_begin(&t[0])
- , m_end(&t[N])
- {}
- template <typename D, size_t N>
- explicit array_proxy(D (&d)[N]) // 元素类型为T兼容类型的数组
- : m_begin(&d[0])
- , m_end(&d[N])
- {
- // 确保D和T大小相同。
- constraint_must_be_same_size(T, D);
- }
- template <typename D>
- array_proxy(array_proxy<D> &d)
- : m_begin(d.begin())
- , m_end(d.end())
- {
- // 确保D和T大小相同。
- constraint_must_be_same_size(T, D);
- }
- // 状态
- public:
- pointer base();
- const_pointer base() const;
- size_type size() const;
- bool empty() const;
- static size_type max_size();
- // 下标索引操作符
- public:
- reference operator [](size_t index);
- const_reference operator [](size_t index) const;
- // 迭代操作
- public:
- pointer begin();
- pointer end();
- const_pointer begin() const;
- const_pointer end() const;
- // 数据成员
- private:
- pointer const m_begin;
- pointer const m_end;
- // 声明但不予实现
- private:
- array_proxy &operator =(array_proxy const &);
- };
第一个构造函数将成员指针m_begin和m_end置为指向数组的开始和超过结尾一个元素的位置。
使用array_proxy,我们可以将process_array()函数重写如下:
- void process_array(array_proxy<Base> ab)
- {
- std::for_each(ab.begin(), ab.end(), . . .); // 处理所有元素
- }