我如何删除类似的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在花括号内有相同的代码。这是重复代码并且可能导致具有复杂逻辑的长函数的维护问题

有办法避免这种代码重复吗?

51392 次浏览

是的,可以避免代码重复。你需要使用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。大多数时候它们都是一行程序,所以代码复制不是问题。

有关详细解释,请参见标题“避免在const和非const成员函数中重复”。第23页,第3项“尽可能使用const”;在有效的c++ , 3d ed由Scott Meyers, ISBN-13: 9780321334879。

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对此进行了深入的讨论。)

比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实例,这就是为什么它是私有的。私有方法可能会破坏外部接口的不变量(在这种情况下,所需的不变量是“一个const对象不能通过引用它所拥有的对象来修改”)。

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

顺便说一下,我很欣赏梅耶斯的解决方案。我对此没有哲学上的异议。不过,就我个人而言,我更喜欢一点点受控的重复,以及只能在某些严格控制的情况下调用的私有方法,而不是看起来像线噪声的方法。选择你的毒药并坚持下去。

[编辑:Kevin已经正确地指出,_getZ可能想要调用一个进一步的方法(比如generateZ),该方法与getZ一样是const特殊化的。在这种情况下,_getZ将看到const Z&并且必须在返回前对其进行const_cast。这仍然是安全的,因为样板访问器监管一切,但它的安全性并不是特别明显。此外,如果你这样做,然后改变generateZ总是返回const,那么你也需要改变getZ总是返回const,但编译器不会告诉你这样做。

关于编译器的后一点也适用于Meyers推荐的模式,但关于不明显的const_cast的第一点则不适用。所以总的来说,我认为如果_getZ需要一个const_cast作为它的返回值,那么这个模式就会比Meyers的模式损失很多。因为与Meyers相比,它也有缺点,我想在这种情况下我会改用他的。从一个重构到另一个很容易——它不会影响类中的任何其他有效代码,因为只有无效代码和样板调用_getZ。

把逻辑移到私有方法中,只在getter中做“获取引用并返回”的事情怎么样?实际上,我对简单getter函数中的静态类型转换和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()这样的公共方法获取X的成员(其中总是需要一个const版本和非const版本),要么可以将此函数作为朋友。但是我不喜欢朋友。

[编辑:删除了测试期间添加的不需要的cstdio。]

我认为Scott Meyers的解决方案可以在c++ 11中通过使用tempate helper函数进行改进。这使得意图更加明显,并且可以被许多其他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)...));
}

可以通过以下方式使用此helper函数。

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,因为变量模板

很好的问题和答案。我有另一个解决方案,不使用类型转换:

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

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

要添加到jwfearn和kevin提供的解决方案,下面是函数返回shared_ptr时对应的解决方案:

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

我这样做是为了一个朋友,他合理地证明了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;
}

我建议使用私有helper静态函数模板,如下所示:

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

没有找到我要找的东西,所以我自己卷了一些…

这个方法有点啰嗦,但优点是可以一次性处理多个同名(和返回类型)重载方法:

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

不幸的是,一旦开始重载名称,这种情况就会崩溃(函数指针参数的参数列表似乎在那时无法解决,因此它无法找到与函数参数匹配的参数)。尽管你也可以用模板来解决这个问题:

  template<typename... Ts>
auto* getp(Ts... args) { return pwrap<int, Ts...>(&C::getp, args...); }

但是const方法的引用参数与模板的明显的按值参数不匹配,它就崩溃了。不知道为什么。这是为什么

使用预处理器是作弊吗?

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


};

它不像模板或类型转换那么花哨,但它确实使您的意图(“这两个函数是相同的”)非常明确。

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

对于那些(像我一样)

  • 使用c++ 17
  • 要添加最少的样板文件/ repeat和
  • 不介意使用(当等待元类时…),

下面是另一种说法:

#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都是正常的(根据编译器浏览器)。

令我惊讶的是,有这么多不同的答案,但几乎所有的答案都依赖于沉重的模板魔法。模板功能强大,但有时宏在简洁方面胜过模板。最大的通用性通常通过两者结合来实现。

我写了一个宏FROM_CONST_OVERLOAD(),它可以放在非const函数中调用const函数。

使用示例:

class MyClass
{
private:
std::vector<std::string> data = {"str", "x"};


public:
// Works for references
const std::string& GetRef(std::size_t index) const
{
return data[index];
}


std::string& GetRef(std::size_t index)
{
return FROM_CONST_OVERLOAD( GetRef(index) );
}




// Works for pointers
const std::string* GetPtr(std::size_t index) const
{
return &data[index];
}


std::string* GetPtr(std::size_t index)
{
return FROM_CONST_OVERLOAD( GetPtr(index) );
}
};

简单且可重用的实现:

template <typename T>
T& WithoutConst(const T& ref)
{
return const_cast<T&>(ref);
}


template <typename T>
T* WithoutConst(const T* ptr)
{
return const_cast<T*>(ptr);
}


template <typename T>
const T* WithConst(T* ptr)
{
return ptr;
}


#define FROM_CONST_OVERLOAD(FunctionCall) \
WithoutConst(WithConst(this)->FunctionCall)

解释:

在许多回答中,避免在非const成员函数中代码重复的典型模式是:

return const_cast<Result&>( static_cast<const MyClass*>(this)->Method(args) );

使用类型推断可以避免很多这种样板文件。首先,const_cast可以封装在WithoutConst()中,它推断其参数的类型并删除const限定符。其次,可以在WithConst()中使用类似的方法对this指针进行const限定,从而可以调用const重载方法。

其余部分是一个简单的宏,它在调用前加上正确限定的this->,并从结果中删除const。由于宏中使用的表达式几乎总是一个简单的带有1:1转发参数的函数调用,因此宏的缺点(如多重求值)并没有发挥作用。省略号和__VA_ARGS__也可以使用,但不应该被需要,因为逗号(作为参数分隔符)出现在括号内。

这种方法有几个好处:

  • 最小和自然的语法——只需将调用包装在FROM_CONST_OVERLOAD( )
  • 不需要额外的成员函数
  • 兼容c++ 98
  • 简单的实现,没有模板元编程和零依赖
  • 可扩展:可以添加其他const关系(如const_iteratorstd::shared_ptr<const T>等)。为此,只需重载相应类型的WithoutConst()

限制:此解决方案针对非const重载与const重载完全相同的场景进行了优化,因此参数可以1:1转发。如果你的逻辑不同,并且你没有通过this->Method(args)调用const版本,你可以考虑其他方法。

我提出了一个宏,自动生成const/非const函数对。

class A
{
int x;
public:
MAYBE_CONST(
CV int &GetX() CV {return x;}
CV int &GetY() CV {return y;}
)


//   Equivalent to:
// int &GetX() {return x;}
// int &GetY() {return y;}
// const int &GetX() const {return x;}
// const int &GetY() const {return y;}
};

有关实现,请参阅答案的末尾。

MAYBE_CONST的实参被复制。在第一个副本中,CV被替换为空;在第二个副本中,它被替换为const

CV可以在宏参数中出现的次数没有限制。

不过有一点小小的不便。如果CV出现在括号内,则这对括号必须加上CV_IN前缀:

// Doesn't work
MAYBE_CONST( CV int &foo(CV int &); )


// Works, expands to
//         int &foo(      int &);
//   const int &foo(const int &);
MAYBE_CONST( CV int &foo CV_IN(CV int &); )

实现:

#define MAYBE_CONST(...) IMPL_CV_maybe_const( (IMPL_CV_null,__VA_ARGS__)() )
#define CV )(IMPL_CV_identity,
#define CV_IN(...) )(IMPL_CV_p_open,)(IMPL_CV_null,__VA_ARGS__)(IMPL_CV_p_close,)(IMPL_CV_null,


#define IMPL_CV_null(...)
#define IMPL_CV_identity(...) __VA_ARGS__
#define IMPL_CV_p_open(...) (
#define IMPL_CV_p_close(...) )


#define IMPL_CV_maybe_const(seq) IMPL_CV_a seq IMPL_CV_const_a seq


#define IMPL_CV_body(cv, m, ...) m(cv) __VA_ARGS__


#define IMPL_CV_a(...) __VA_OPT__(IMPL_CV_body(,__VA_ARGS__) IMPL_CV_b)
#define IMPL_CV_b(...) __VA_OPT__(IMPL_CV_body(,__VA_ARGS__) IMPL_CV_a)


#define IMPL_CV_const_a(...) __VA_OPT__(IMPL_CV_body(const,__VA_ARGS__) IMPL_CV_const_b)
#define IMPL_CV_const_b(...) __VA_OPT__(IMPL_CV_body(const,__VA_ARGS__) IMPL_CV_const_a)

pre - c++ 20实现,不支持CV_IN:

#define MAYBE_CONST(...) IMPL_MC( ((__VA_ARGS__)) )
#define CV ))((


#define IMPL_MC(seq) \
IMPL_MC_end(IMPL_MC_a seq) \
IMPL_MC_end(IMPL_MC_const_0 seq)


#define IMPL_MC_identity(...) __VA_ARGS__
#define IMPL_MC_end(...) IMPL_MC_end_(__VA_ARGS__)
#define IMPL_MC_end_(...) __VA_ARGS__##_end


#define IMPL_MC_a(elem) IMPL_MC_identity elem IMPL_MC_b
#define IMPL_MC_b(elem) IMPL_MC_identity elem IMPL_MC_a
#define IMPL_MC_a_end
#define IMPL_MC_b_end


#define IMPL_MC_const_0(elem)       IMPL_MC_identity elem IMPL_MC_const_a
#define IMPL_MC_const_a(elem) const IMPL_MC_identity elem IMPL_MC_const_b
#define IMPL_MC_const_b(elem) const IMPL_MC_identity elem IMPL_MC_const_a
#define IMPL_MC_const_a_end
#define IMPL_MC_const_b_end

如果你不喜欢常量强制转换,我使用这个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

虽然这里的大多数答案都建议使用const_cast,但CppCoreGuidelines对此有部分:

相反,更喜欢共享实现。通常,你可以让非const函数调用const函数。然而,当存在复杂的逻辑时,这可能导致以下模式,仍然诉诸于const_cast:

class Foo {
public:
// not great, non-const calls const version but resorts to const_cast
Bar& get_bar()
{
return const_cast<Bar&>(static_cast<const Foo&>(*this).get_bar());
}
const Bar& get_bar() const
{
/* the complex logic around getting a const reference to my_bar */
}
private:
Bar my_bar;
};
虽然这个模式在正确应用时是安全的,因为 调用者必须一开始就有一个非const对象,这并不理想 因为安全性很难作为检查规则自动执行。

相反,更倾向于将公共代码放在公共helper函数中—— 让它成为一个模板,这样它就可以推导出const。这个不需要任何东西 Const_cast在所有:

class Foo {
public:                         // good
Bar& get_bar()       { return get_bar_impl(*this); }
const Bar& get_bar() const { return get_bar_impl(*this); }
private:
Bar my_bar;


template<class T>           // good, deduces whether T is const or non-const
static auto& get_bar_impl(T& t)
{ /* the complex logic around getting a possibly-const reference to my_bar */ }
};

注意:不要在模板中做大量不依赖的工作,这会导致代码膨胀。例如,如果get_bar_impl的全部或部分可以是非依赖的,并分解成一个公共的非模板函数,则可以进一步改进,从而可能大大减少代码大小。

c++ 23通过推导出这个更新了这个问题的最佳答案:

struct s {
auto && f(this auto && self) {
// all the common code goes here
}
};

单个函数模板可作为普通成员函数调用,并为您推导正确的引用类型。没有错误的类型转换,没有为一个概念上的东西编写多个函数。