如何删除相似的const和非const成员函数之间的代码重复?
假设我有以下"类X",我想在其中返回对内部成员的访问权限:
class Z { // details }; class X { std::vector<Z> vecZ; public: Z& Z(size_t index) { // massive amounts of code for validating index Z& ret = vecZ[index]; // even more code for determining that the Z instance // at index is *exactly* the right sort of Z (a process // which involves calculating leap years in which // religious holidays fall on Tuesdays for // the next thousand years or so) return ret; } const Z& Z(size_t index) const { // identical to non-const X::Z(), except printed in // a lighter shade of gray since // we're running low on toner by this point } };
两个成员函数" X :: Z()"和" X :: Z()const"在花括号内具有相同的代码。这是重复的代码,可能会对具有复杂逻辑的长功能造成维护问题。
有没有办法避免此代码重复?
解决方案
是的,可以避免代码重复。我们需要使用const成员函数来具有逻辑,并使非const成员函数调用const成员函数,并将返回值重新投射到非const引用(如果函数返回指针,则返回指针):
class X { std::vector<Z> vecZ; public: const Z& Z(size_t index) const { // same really-really-really long access // and checking code as in OP // ... return vecZ[index]; } Z& Z(size_t index) { // One line. One ugly, ugly line - but just one line! return const_cast<Z&>( static_cast<const X&>(*this).Z(index) ); } #if 0 // A slightly less-ugly version Z& Z(size_t index) { // Two lines -- one cast. This is slightly less ugly but takes an extra line. const X& constMe = *this; return const_cast<Z&>( constMe.Z(index) ); } #endif };
注意:重要的是不要将逻辑放在非const函数中,而让const函数调用非const函数-这可能会导致不确定的行为。原因是常量类实例被转换为非常量实例。非const成员函数可能会意外地修改该类,而C ++标准声明的类将导致未定义的行为。
这篇DDJ文章展示了一种使用模板特化的方法,该方法不需要我们使用const_cast。对于这样一个简单的功能,实际上并不需要它。
boost :: any_cast(在某一时刻,不再使用)使用const版本的const_cast调用非const版本,以避免重复。但是,我们不能在非const版本上强加const语义,因此我们必须非常小心。
最后,只要两个代码片段彼此直接重叠,就可以进行一些代码复制。
通常,需要const和非const版本的成员函数是getter和setter。在大多数情况下,它们是单行的,因此代码重复不是问题。
有关详细说明,请参阅第119页码"避免在const和Non-const成员函数中重复"。 23,在有效的C ++中,第3项"尽可能使用const",Scott Meyers编辑,ISDN-13:9780321334879.
这是Meyers的解决方案(简化):
struct C { const char & get() const { return c; } char & get() { return const_cast<char &>(static_cast<const C &>(*this).get()); } char c; };
这两个强制类型转换和函数调用可能很难看,但这是正确的。迈耶斯对此做了详尽的解释。
比Meyers更为冗长,但我可以这样做:
class X { private: // This method MUST NOT be called except from boilerplate accessors. Z &_getZ(size_t index) const { return something; } // boilerplate accessors public: Z &getZ(size_t index) { return _getZ(index); } const Z &getZ(size_t index) const { return _getZ(index); } };
private方法具有令人讨厌的特性,即它为const实例返回非const Z&,这就是为什么它是private的原因。私有方法可能会破坏外部接口的不变性(在这种情况下,所需的不变性是"无法通过对const对象的引用来修改其const对象")。
请注意,注释是模式_getZ的接口的一部分,该接口指定调用它永远无效(很明显,除了访问器之外):这样做没有任何好处,因为它会多输入1个字符并且不会导致用更小或者更快速的代码。调用该方法等效于使用const_cast调用其中一个访问器,并且我们也不想这样做。如果我们担心使错误变得明显(这是一个公平的目标),则将其称为const_cast_getZ而不是_getZ。
顺便说一句,我感谢迈耶斯的解决方案。我对此没有哲学上的异议。但是,就我个人而言,我喜欢一点点可控的重复,并且只喜欢在某些严格控制的情况下才能调用的私有方法,而不是看起来像线路噪声的方法。选择你的毒药并坚持下去。
[编辑:凯文正确地指出,_getZ可能想要调用另一种方法(例如generateZ),该方法以与getZ相同的方式进行const专业化。在这种情况下,_getZ将看到const Z&,并且必须在返回之前进行const_cast。由于样板访问器可以管理所有操作,因此这仍然是安全的,但并不是很明显它是安全的。此外,如果我们执行此操作,然后在以后将generateZ更改为始终返回const,则还需要将getZ更改为始终返回const,但是编译器不会告诉我们这样做。
关于编译器的后一点也适用于Meyers的推荐模式,但是关于非显而易见的const_cast的第一点不是。因此,总的来说,我认为如果_getZ的返回值确实需要const_cast,则此模式将失去其与Meyers相比的很多值。由于与Meyers相比,它也具有劣势,因此我认为在这种情况下,我将转而使用他。从一个到另一个的重构很容易-它不会影响该类中的任何其他有效代码,因为只有无效代码和样板代码才会调用_getZ。
将逻辑移到私有方法中,仅在吸气剂内部进行"获取引用并返回"操作,该怎么办?实际上,对于一个简单的getter函数中的static和const强制转换,我会感到非常困惑,除了极少数情况下,我会认为这很丑陋!
我们也可以使用模板解决此问题。此解决方案有点丑陋(但是丑陋隐藏在.cpp文件中),但是它确实提供了编译器对常量的检查,并且没有代码重复。
.h文件:
#include <vector> class Z { // details }; class X { std::vector<Z> vecZ; public: const std::vector<Z>& GetVector() const { return vecZ; } std::vector<Z>& GetVector() { return vecZ; } Z& GetZ( size_t index ); const Z& GetZ( size_t index ) const; };
.cpp文件:
#include "constnonconst.h" template< class ParentPtr, class Child > Child& GetZImpl( ParentPtr parent, size_t index ) { // ... massive amounts of code ... // Note you may only use methods of X here that are // available in both const and non-const varieties. Child& ret = parent->GetVector()[index]; // ... even more code ... return ret; } Z& X::GetZ( size_t index ) { return GetZImpl< X*, Z >( this, index ); } const Z& X::GetZ( size_t index ) const { return GetZImpl< const X*, const Z >( this, index ); }
我可以看到的主要缺点是,因为该方法的所有复杂实现都在全局函数中,所以我们或者需要使用上面的GetVector()之类的公共方法(通常总是需要一个const和非const版本),也可以将此函数作为朋友。但是我不喜欢朋友。
[编辑:在测试过程中删除了不必要的包含cstdio的内容。]