如何删除类似的const和非const成员函数之间的代码重复?
比方说,我有以下class 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
在大括号中有相同的代码。 这是重复的代码,可能会导致长时间的复杂逻辑function的维护问题 。
有没有办法避免这种代码重复?
有关详细的解释,请参阅标题为“在const
和非常量成员函数中避免重复”一节的第26页。 23,在第3项“尽可能使用const
”中,Scott Meyers的Effective C ++ ,3d ed,ISBN-13:9780321334879。
这里是迈尔斯的解决scheme(简化):
struct C { const char & get() const { return c; } char & get() { return const_cast<char &>(static_cast<const C &>(*this).get()); } char c; };
两个演员和function调用可能是丑陋的,但它是正确的。 迈耶斯有一个彻底的解释,为什么。
是的,可以避免代码重复。 你需要使用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 ++标准将导致未定义的行为。
我认为Scott Meyers的解决scheme可以通过使用临时帮助函数在C ++ 11中进行改进。 这使得这个意图更加明显,可以被其他许多获得者重用。
template <typename T> struct NonConst {typedef T type;}; template <typename T> struct NonConst<T const> {typedef T type;}; //by value template <typename T> struct NonConst<T const&> {typedef T& type;}; //by reference template <typename T> struct NonConst<T const*> {typedef T* type;}; //by pointer template <typename T> struct NonConst<T const&&> {typedef T&& type;}; //by rvalue-reference template<typename TConstReturn, class TObj, typename... TArgs> typename NonConst<TConstReturn>::type likeConstVersion( TObj const* obj, TConstReturn (TObj::* memFun)(TArgs...) const, TArgs&&... args) { return const_cast<typename NonConst<TConstReturn>::type>( (obj->*memFun)(std::forward<TArgs>(args)...)); }
这个帮助函数可以用下面的方法。
struct T { int arr[100]; int const& getElement(size_t i) const{ return arr[i]; } int& getElement(size_t i) { return likeConstVersion(this, &T::getElement, i); } };
第一个参数总是这个指针。 第二个是指向要调用的成员函数的指针。 之后,可以传递任意数量的附加参数,以便将它们转发给函数。 这需要C ++ 11,因为可变模板。
比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); } };
私有方法具有不可取的属性,它返回一个非const Z&为一个const实例,这就是为什么它是私人的。 私有方法可能会破坏外部接口的不variables(在这种情况下,所需的不variables是“一个const对象不能通过通过它获得的对象来修改它的对象)”。
请注意,注释是该模式的一部分 – _getZ的接口指定它永远不会有效调用它(除了访问器,显然):无论如何,没有可以想象的好处,因为它是1个字符键入,不会导致更小或更快的代码。 调用方法相当于用const_cast调用其中一个访问器,而且你也不想这样做。 如果你担心明显的错误(这是一个公平的目标),那么把它叫做const_cast_getZ而不是_getZ。
顺便说一下,我很欣赏迈尔斯的解决scheme。 我没有哲学上的反对意见。 然而就我个人而言,我更喜欢一点控制的重复,而一种私人的方法只能在某些严格控制的情况下才能通过看起来像线噪声的方法来调用。 select你的毒药,坚持下去。
[编辑:凯文正确地指出,_getZ可能想要调用一个更进一步的方法(比如说generateZ),这是一个常量 – 专门用getZ方法。 在这种情况下,_getZ会看到一个const Z&,并且必须在返回之前const_cast它。 这仍然是安全的,因为样板文件访问者警惕一切,但它并不是很明显,它是安全的。 此外,如果你这样做,然后改变generateZ总是返回const,那么你也需要改变getZ总是返回const,但编译器不会告诉你,你做。
关于编译器的后面的观点也适用于Meyers推荐的模式,但关于一个不明显的const_cast的第一点不是。 所以总的来说,我认为,如果_getZ需要一个const_cast作为它的返回值,那么这个模式就会失去它比Meyers更多的价值。 既然它也比梅耶斯有劣势,我想我会在那种情况下转向他。 重构从一个到另一个很容易 – 它不会影响类中的任何其他有效的代码,因为只有无效的代码和样板调用_getZ。]
很好的问题和很好的答案。 我有另一个解决scheme,不使用转换:
class X { private: std::vector<Z> v; template<typename InstanceType> static auto get(InstanceType& instance, std::size_t i) -> decltype(instance.get(i)) { // massive amounts of code for validating index // the instance variable has to be used to access class members return instance.v[i]; } public: const Z& get(std::size_t i) const { return get(*this, i); } Z& get(std::size_t i) { return get(*this, i); } };
然而,它需要一个静态成员的丑陋和在其中使用instance
variables的需要。
我没有考虑到这个解决scheme所有可能的(消极的)影响。 请让我知道,如果有的话。
你也可以用模板解决这个问题。 这个解决scheme有点丑陋(但丑陋隐藏在.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()这样的公共方法来获得X的成员(其中总是需要一个const和非const版本),或者你可以使这个function成为朋友。 但我不喜欢朋友。
[编辑:删除不需要包括在testing过程中添加的cstdio。]
如何将逻辑转化为私有方法,并且只在获取方中执行“获取引用和返回”的东西? 实际上,我会对一个简单的getter函数里的静态和常量强制转换感到困惑,除非是非常罕见的情况,否则我会认为这很丑陋!
我这样做的一个朋友谁正确地使用const_cast
的使用…不知道我可能会做这样的事情(不是很优雅):
#include <iostream> class MyClass { public: int getI() { std::cout << "non-const getter" << std::endl; return privateGetI<MyClass, int>(*this); } const int getI() const { std::cout << "const getter" << std::endl; return privateGetI<const MyClass, const int>(*this); } private: template <class C, typename T> static T privateGetI(C c) { //do my stuff return c._i; } int _i; }; int main() { const MyClass myConstClass = MyClass(); myConstClass.getI(); MyClass myNonConstClass; myNonConstClass.getI(); return 0; }
使用预处理器是否作弊?
struct A { #define GETTER_CORE_CODE \ /* line 1 of getter code */ \ /* line 2 of getter code */ \ /* .....etc............. */ \ /* line n of getter code */ // ^ NOTE: line continuation char '\' on all lines but the last B& get() { GETTER_CORE_CODE } const B& get() const { GETTER_CORE_CODE } #undef GETTER_CORE_CODE };
它不像模板或模板那样华丽,但它确实使你的意图(“这两个function是相同的”)非常明确。
通常,您需要const和non-const版本的成员函数是getter和setter。 大多数时候,他们是单行的,所以代码重复不是问题。
要添加jwfearn和kevin提供的解决scheme,下面是函数返回shared_ptr时的相应解决scheme:
struct C { shared_ptr<const char> get() const { return c; } shared_ptr<char> get() { return const_pointer_cast<char>(static_cast<const C &>(*this).get()); } shared_ptr<char> c; };
我会build议一个私人帮手静态函数模板,像这样:
class X { std::vector<Z> vecZ; // ReturnType is explicitly 'Z&' or 'const Z&' // ThisType is deduced to be 'X' or 'const X' template <typename ReturnType, typename ThisType> static ReturnType Z_impl(ThisType& self, size_t index) { // massive amounts of code for validating index ReturnType ret = self.vecZ[index]; // even more code for determining, blah, blah... return ret; } public: Z& Z(size_t index) { return Z_impl<Z&>(*this, index); } const Z& Z(size_t index) const { return Z_impl<const Z&>(*this, index); } };
没有find我要找的东西,所以我推出了一些我自己的…
这一个有点罗嗦,但有一个同时处理同名(和返回types)的许多重载方法的优势:
struct C { int x[10]; int const* getp() const { return x; } int const* getp(int i) const { return &x[i]; } int const* getp(int* p) const { return &x[*p]; } int const& getr() const { return x[0]; } int const& getr(int i) const { return x[i]; } int const& getr(int* p) const { return x[*p]; } template<typename... Ts> auto* getp(Ts... args) { auto const* p = this; return const_cast<int*>(p->getp(args...)); } template<typename... Ts> auto& getr(Ts... args) { auto const* p = this; return const_cast<int&>(p->getr(args...)); } };
如果每个名称只有一个const
方法,但仍有很多方法可以复制,那么你可能更喜欢这样:
template<typename T, typename... Ts> auto* pwrap(T const* (C::*f)(Ts...) const, Ts... args) { return const_cast<T*>((this->*f)(args...)); } int* getp_i(int i) { return pwrap(&C::getp_i, i); } int* getp_p(int* p) { return pwrap(&C::getp_p, p); }
不幸的是,只要你开始重载名字,就会崩溃(函数指针参数的参数列表在这一点上似乎是未解决的,所以它找不到函数参数的匹配项)。 尽pipe你也可以通过模板来解决这个问题:
template<typename... Ts> auto* getp(Ts... args) { return pwrap<int, Ts...>(&C::getp, args...); }
但是const
方法的引用参数与模板的明显的by-value参数无法匹配,并且会中断。 不知道为什么。 这是为什么 。
这篇DDJ文章展示了一种使用模板专门化的方法,不需要你使用const_cast。 对于这样一个简单的function,它确实不是必需的。
boost :: any_cast(在某一点,它不再)使用从const版本const_cast调用非const版本,以避免重复。 你不能在非const的版本上加上const语义,但是你必须非常小心。
最后,只要两个代码段直接相互重叠,代码复制就可以了。