如何消除相似的const和非const成员函数之间的代码重复?

311
假设我有以下的`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 在大括号内部有相同的代码。这是重复的代码,并且可能导致具有复杂逻辑的长函数的维护问题
有没有办法避免这种代码重复?

在这个例子中,我会在const情况下返回一个值,所以你不能在下面进行重构。int Z() const { return z; } - Matt Price
2
对于基本类型,你是完全正确的!我的第一个例子不太好。我们假设我们返回一些类实例。 (我更新了问题以反映这一点。) - Kevin
21个回答

246

如需详细解释,请查看Effective C++,第3版,作者为Scott Meyers,ISBN-13:9780321334879,在第3项“尽可能使用const”中的第3项“避免在const和非const成员函数中重复使用”,第23页。

alt text

这是Meyers的解决方案(简化版):

struct C {
  const char & get() const {
    return c;
  }
  char & get() {
    return const_cast<char &>(static_cast<const C &>(*this).get());
  }
  char c;
};

这两个类型转换和函数调用可能看起来很丑陋,但在非const方法中是正确的,因为这意味着对象一开始就不是const。(Meyers对此进行了全面讨论。)


64
遵循Scott Meyers的建议从未有人被解雇过 :-) - Steve Jessop
12
Witkamp正确指出,通常使用const_cast是不好的。但这是一个特殊情况,在Meyers的解释下可以使用。@Adam:将ROM转为const是可以的。将const等同于ROM显然是无意义的,因为任何人都可以毫不费力地将非const转换为const:这等同于选择不修改某些内容。 - Steve Jessop
49
通常建议使用const_cast而不是static_cast来添加const,因为它可以防止您意外更改类型。 - Greg Rogers
11
@HelloGoodbye:我认为Meyers假定类接口的设计者具有一定的智力。如果get()const返回被定义为const对象的内容,则根本不应该有非const版本的get()。实际上,随着时间的推移,我的想法已经改变:模板解决方案是避免重复并获得编译器检查的const正确性的唯一方法,因此个人不再使用const_cast来避免重复代码,而是选择将重复的代码放入函数模板中,或者让它保持重复。 - Steve Jessop
18
使用 C++17 中的 std::as_const() 更好了。 - Deduplicator
显示剩余10条评论

98

C++17更新了这个问题的最佳解答:

T const & f() const {
    return something_complicated();
}
T & f() {
    return const_cast<T &>(std::as_const(*this).f());
}

这种方法的优点是:

  • 操作显而易见
  • 代码开销最小——只需一行即可完成
  • 很难出错(只有在意外情况下才会强制转换volatile,但volatile是一个罕见的限定符)

如果您想采用完全的推导路线,则可以通过使用辅助函数来实现。

template<typename T>
constexpr T & as_mutable(T const & value) noexcept {
    return const_cast<T &>(value);
}
template<typename T>
constexpr T * as_mutable(T const * value) noexcept {
    return const_cast<T *>(value);
}
template<typename T>
constexpr T * as_mutable(T * value) noexcept {
    return value;
}
template<typename T>
void as_mutable(T const &&) = delete;

现在你甚至不能搞砸volatile,使用方法如下:
decltype(auto) f() const {
    return something_complicated();
}
decltype(auto) f() {
    return as_mutable(std::as_const(*this).f());
}

请注意,“as_mutable”删除了const rvalue重载(通常更可取),如果“f()”返回T而不是T&,则会阻止最后一个示例的工作。 - Max Truxa
3
是的,这是一件好事情。如果只是编译,我们会有一个悬垂引用。在f()返回T的情况下,我们不想要两个重载函数,只需要const版本就足够了。 - David Stone
1
如果一个方法返回 T const*,那么它会绑定到 T const* const&& 而不是绑定到 T const* const&(至少在我的测试中是这样的)。我不得不为返回指针的方法添加一个 T const* 重载作为参数类型。 - monkey0506
在进一步思考删除第一个T&&重载的原因时,我选择使用单独的方法。虽然认为编译器会捕捉到返回悬空引用的任何错误是很好的,但我认为最好在需要时稍微详细一些。 - monkey0506
3
我已更新我的回答,支持指针和引用。 - David Stone
显示剩余4条评论

74

是的,避免代码重复是有可能的。您需要使用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++标准,会导致未定义的行为。


5
哇...太糟糕了。你刚刚增加了代码量,减少了清晰度并添加了两个恶心的const_cast<>。也许你有一个实际上有意义的例子吗? - Shog9
17
嘿,别动它!虽然它可能很丑,但根据斯科特·迈尔斯的说法,这(几乎)是正确的方法。请参阅《Effective C++》第3版中“避免在const和非const成员函数中重复”的第3项。 - jwfearn
21
虽然我知道解决方案可能不太美观,但请想象一下决定返回什么的代码长达50行。那么重复是非常不可取的,特别是当你必须重构代码时。在我的职业生涯中,我已经遇到过许多类似情况。 - Kevin
10
这里与 Meyers 的区别在于 Meyers 使用了 static_cast<const X&>(*this)。const_cast 用于去除 const,而非添加 const。 - Steve Jessop
13
我们知道该对象最初并非以const方式创建,因为它是非const对象的非const成员,我们之所以知道这一点是因为我们在该对象的非const方法中。编译器不会进行这种推断,而是遵循保守的规则。如果不是针对这种情况,你认为const_cast存在的理由是什么呢? - Caleth
显示剩余6条评论

37

我认为在C++11中可以通过使用模板帮助函数来改进Scott Meyers的解决方案。这使意图更加明显,并且可以重用于许多其他getter。

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);
   }
};

第一个参数始终是this指针。第二个参数是要调用的成员函数的指针。之后可以传递任意数量的其他参数,以便将它们转发到函数中。 这需要C++11支持可变参数模板。


3
很遗憾,我们没有std::remove_bottom_const可以与std::remove_const配合使用。 - TBBle
2
@v.oddou: std::remove_const<int const&>int const &(移除顶层的 const 限定符),因此在这个答案中需要使用 NonConst<T> 进行转换。假设有一个 std::remove_bottom_const,它可以移除底层的 const 限定符,并且可以精确地执行 NonConst<T> 在这里所做的操作:std::remove_bottom_const<int const&>::type => int& - TBBle
4
如果getElement被重载,那么这个解决方案效果不佳。在没有显式指定模板参数的情况下,无法解析函数指针。为什么? - John
@John 这是 C++ 成员函数指针重载解析的问题(请参见 https://dev59.com/V3A85IYBdhLWcg3wF_m0)。目前,这只能在调用方解决。 但您不必指定所有模板参数。只有第一个 TConstReturn 也可以工作。例如:return likeConstVersion<int const&>(this, &T::getElement, i); - Pait
1
你需要修复你的答案,使用C++11完美转发:likeConstVersion(TObj const* obj, TConstReturn (TObj::*memFun)(TArgs...) const, TArgs&&... args) { return const_cast::type>((obj->*memFun)(std::forward(args)...)); }完整代码请参考:https://gist.github.com/BlueSolei/bca26a8590265492e2f2760d3cefcf83 - ShaulF
显示剩余4条评论

33

好问题和好答案。我有另一个解决方案,它不使用强制转换:

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变量。

我没有考虑到这种解决方案可能产生的所有(负面)影响。如果有,请告诉我。


7
让我们来看一个简单的事实,你添加了更多的样板代码。如果有什么需要的话,这应该被用作一个例子,说明为什么语言需要一种修改函数限定符以及返回类型的方式。auto get(std::size_t i) -> auto(const), auto(&&)。为什么要使用 '&&'?噢,这样我就可以说:auto foo() -> auto(const), auto(&&) = delete; - kfsone
1
@kfsone 语法应包含 "this" 关键字。我建议使用 template< typename T > auto myfunction(T this, t args) -> decltype(ident)。这个关键字将被识别为隐式对象实例参数,使编译器认识到 myfunction 是 T 的成员。T 将在调用时自动推导出来,始终是类的类型,但带有免费 cv 修饰符。 - v.oddou
4
该解决方案相对于 const_cast 的优点在于允许返回 iteratorconst_iterator - Jarod42
1
如果实现被移动到cpp文件中(并且由于不应该是平凡的方法,这可能是情况),则“static”可以在文件范围而不是类范围内完成。 :-) - Jarod42
1
@Medran 谢谢。这个答案是在2013年写的:冗长的尾随decltype可能不再需要,decltype(auto)可能会完成它。 - gd1
显示剩余10条评论

32
C++23对这个问题进行了更新,感谢显式对象参数提供的最佳答案。
struct s {
    auto && f(this auto && self) {
        // all the common code goes here
    }
};

一个单一的函数模板可以像普通成员函数一样被调用,并为您推断出正确的引用类型。不需要进行错误的强制转换,也不需要为概念上是同一件事情的多个函数编写多个函数。
注意:此功能由P0847:推断此事添加。

2
值得注意的是,这个成员函数还接受volatile限定的对象,并且它接受表达式的任何值类别。这使得它比仅仅是const/非const函数对更加强大。 - undefined

24

比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&,这就是它被设置为私有的原因。私有方法可能会破坏外部接口的不变性(在本例中,期望的不变性是“不能通过从其拥有-a的对象获取的引用来修改const对象”)。

请注意,注释是模式的一部分——_getZ的接口指定不调用它是无效的(除了访问器,显然):因为根本没有可想象的好处,而且它需要输入更多字符,并且不会导致更小或更快的代码。调用该方法等同于使用const_cast调用其中一个访问器,你也不希望那样做。如果你担心让错误变得明显(这是一个公正的目标),那么将其称为const_cast_getZ而不是_getZ。

顺便说一下,我欣赏Meyers的解决方案。我对此没有哲学上的反对意见。然而,个人而言,我更喜欢少许可控制的重复和仅在某些严格受控情况下调用的私有方法,而不是看起来像行噪声的方法。选择你的毒药并坚持下去。

[编辑:Kevin正确指出了_getZ可能想调用一个进一步的方法(比如generateZ),它以同样的方式进行const特化。在这种情况下,_getZ将看到一个const Z&并且必须在返回之前对其进行const_cast。这仍然是安全的,因为样板访问器会管理所有事情,但它不是非常明显是安全的。此外,如果你这样做,然后稍后将generateZ更改为始终返回const,那么你也需要将getZ更改为始终返回const,但编译器不会告诉你这样做。

关于编译器的后一个问题对Meyers推荐的模式也是正确的,但是有关非显而易见的const_cast的第一个问题则不是。因此,在平衡中,如果_getZ最终需要一个const_cast作为返回值,则此模式相对于Meyers的价值会大大降低。由于它也比Meyers的方案具有劣势,因此我认为在这种情况下我会切换到他的方案。从一个重构到另一个很容易——它不会影响类中的任何其他有效代码,因为只有无效代码和样板调用了_getZ。]


5
这仍然存在一个问题,即返回的东西可能对于X的一个常量实例是常数。在这种情况下,您仍需要在_getZ(...)中使用const_cast。如果后续开发人员滥用它,仍可能导致未定义行为。如果返回的东西是“可变的”,那么这是一个好的解决方案。 - Kevin
16
许多情况下这种方法行不通。如果_getZ()函数中的something是一个实例变量会怎样呢?编译器(至少某些编译器)将抱怨说由于_getZ()是const,其中引用的任何实例变量也是const。因此,something将变为const(类型为const Z&),无法转换为Z&。据我(尽管有限)的经验,在这种情况下,大多数情况下something都是一个实例变量。 - Gravity
2
@GravityBringer:那么“something”需要涉及到const_cast。它旨在作为获取常量对象的非常量返回所需的代码的占位符,而不是复制的getter中本应该有的占位符。因此,“something”不仅仅是一个实例变量。 - Steve Jessop
2
我明白了。但这确实降低了该技术的实用性。我想撤销踩,但 SO 不允许我这样做。 - Gravity
1
@HelloGoodbye:那不正确。如果需要const_cast,则不仅需要分别使用const和非const版本。例如,something可能会归结为*some_pointer_data_member(并且假设您希望在const情况下返回const引用),在这种情况下,Meyers的代码引入了一个转换,但实际上您不需要一个。他的代码提供了一个完全通用的模式,但是您编写的大多数实际函数更具体 :-) - Steve Jessop
显示剩余6条评论

8

对于那些(像我一样)使用 c++17 的人,想要添加最少的样板代码/重复,并且不介意使用 (在等待元类的同时...),这里是另一种方法:

#include <utility>
#include <type_traits>

template <typename T> struct NonConst;
template <typename T> struct NonConst<T const&> {using type = T&;};
template <typename T> struct NonConst<T const*> {using type = T*;};

#define NON_CONST(func)                                                     \
    template <typename... T> auto func(T&&... a)                            \
        -> typename NonConst<decltype(func(std::forward<T>(a)...))>::type   \
    {                                                                       \
        return const_cast<decltype(func(std::forward<T>(a)...))>(           \
            std::as_const(*this).func(std::forward<T>(a)...));              \
    }

这基本上是@Pait、@DavidStone和@sh1的回答的混合体(编辑:以及@cdhowie的改进)。它增加的只是一个额外的代码行,仅命名函数(但没有参数或返回类型的重复):

class X
{
    const Z& get(size_t index) const { ... }
    NON_CONST(get)
};

注意:gcc在8.1版本之前无法编译此代码,在clang-5及以上版本以及MSVC-19上可以成功编译(根据编译器资源网站的信息)。

1
这对我来说很顺利。这是一个很好的答案,谢谢! - Short
decltype() 的参数也应该使用 std::forward,以确保在 get() 有不同类型引用的重载情况下使用正确的返回类型,这样才更合适。 - cdhowie
@cdhowie 你能提供一个例子吗? - axxel
@axxel 这个例子有点牵强,但是在这里NON_CONST 宏由于 decltype(func(a...)) 类型中缺少转发而错误地推断了返回类型并进行了错误的 const_cast。将它们替换为 decltype(func(std::forward<T>(a)...)) 可以解决这个问题。(只是因为我从未定义过任何已声明的 X::get 重载函数,所以会出现链接器错误。) - cdhowie
1
感谢@cdhowie,我对您的示例进行了改进,以实际使用非const重载:http://coliru.stacked-crooked.com/a/0cedc7f4e789479e - axxel

8
您还可以使用模板解决此问题。这种解决方案略有些丑陋(但丑陋之处隐藏在.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版本),或者将此函数设置为friend。但我不喜欢使用friend。

4
你可以将复杂的实现功能作为静态成员,以便访问私有成员。该函数只需在类头文件中声明即可,定义可以放在类实现文件中。毕竟,它是类实现的一部分。 - CB Bailey
啊,好主意!我不喜欢模板的东西出现在头部,但是如果在这里使用它可能会使实现变得更简单,那么这样做可能是值得的。 - Andy Balaam
给这个解决方案加上1,但不要重复任何代码,也不要使用任何丑陋的 const_cast(它可能会意外地将本来应该是常量的东西强制转换成不是常量的东西)。 - HelloGoodbye
现在,可以通过模板的推导返回类型来简化此过程(特别是在成员情况下,它可以减少必须在类中复制的内容),这非常有用。 - Davis Herring

6

如果你不喜欢使用const强制类型转换,可以使用这个C ++ 17版本的模板静态辅助函数,它由另一个答案建议,带有可选的SFINAE测试。

#include <type_traits>

#define REQUIRES(...)         class = std::enable_if_t<(__VA_ARGS__)>
#define REQUIRES_CV_OF(A,B)   REQUIRES( std::is_same_v< std::remove_cv_t< A >, B > )

class Foobar {
private:
    int something;

    template<class FOOBAR, REQUIRES_CV_OF(FOOBAR, Foobar)>
    static auto& _getSomething(FOOBAR& self, int index) {
        // big, non-trivial chunk of code...
        return self.something;
    }

public:
    auto& getSomething(int index)       { return _getSomething(*this, index); }
    auto& getSomething(int index) const { return _getSomething(*this, index); }
};

完整版本: https://godbolt.org/z/mMK4r3

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接