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

时间:2008-09-23 20:47:11

标签: c++ class const code-duplication c++-faq

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

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

20 个答案:

答案 0 :(得分:163)

有关详细说明,请参阅标题为“避免重复const和非 - const成员函数”,第28页。 23,在第3项“尽可能使用const”中,在 Effective C ++ 中,由Scott Meyers编辑,ISBN-13:9780321334879。

alt text

这是迈耶斯的解决方案(简化):

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

两个强制转换和函数调用可能很难看,但它是正确的。迈耶斯有详尽的解释原因。

答案 1 :(得分:56)

是的,可以避免代码重复。您需要使用const成员函数来获取逻辑并让非const成员函数调用const成员函数并将返回值重新转换为非const引用(或者如果函数返回指针则返回指针): / p>

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

注意:重要的是你 NOT 将逻辑放在非const函数中并让const函数调用非const函数 - 它可能会导致未定义的行为。原因是常量类实例被转换为非常量实例。非const成员函数可能会意外地修改类,C ++标准声明这将导致未定义的行为。

答案 2 :(得分:31)

我认为Scott Meyers的解决方案可以通过使用tempate helper函数在C ++ 11中得到改进。这使得意图更加明显,可以重复用于许多其他的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-pointer。第二个是指向要调用的成员函数的指针。之后,可以传递任意数量的附加参数,以便将它们转发给函数。 由于可变参数模板,这需要C ++ 11。

答案 3 :(得分:20)

比迈耶斯更冗长,但我可能会这样做:

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

私有方法具有不良属性,它返回非常量Z&amp;对于const实例,这就是为什么它是私有的。私有方法可能会破坏外部接口的不变量(在这种情况下,所需的不变量是“const对象不能通过它获得的引用修改为它具有的对象”)。

请注意,注释是模式的一部分 - _getZ的接口指定调用它永远无效(显然除了访问器):无论如何这样做都没有可能的好处,因为它是另外一个字符来键入和不会导致更小或更快的代码。调用该方法相当于使用const_cast调用其中一个访问器,您也不希望这样做。如果你担心错误明显(这是一个公平的目标),那么称之为const_cast_getZ而不是_getZ。

顺便说一句,我很欣赏迈耶斯的解决方案。我对它没有任何哲学上的反对意见。但就个人而言,我更喜欢一点点的受控重复,以及一种只能在某些严格控制的情况下调用的私有方法,而不是一种看似线噪声的方法。选择你的毒药并坚持下去。

[编辑:Kevin正确地指出_getZ可能想要调用另一个方法(比如generateZ),这个方法与getZ一样是const专用的。在这种情况下,_getZ会看到一个const Z&amp;并且必须在返回之前const_cast它。这仍然是安全的,因为样板访问器可以控制所有内容,但是它的安全性并不明显。此外,如果你这样做,然后将generateZ更改为始终返回const,那么你还需要将getZ更改为始终返回const,但编译器不会告诉您这样做。

关于编译器的后一点也适用于Meyers推荐的模式,但关于非显而易见的const_cast的第一点不是。所以总的来说,我认为如果_getZ结果需要一个const_cast作为其返回值,那么这种模式会失去很多超过Meyers的价值。由于与Meyers相比它也有缺点,我想我会在那种情况下转向他。从一个重构到另一个很容易 - 它不会影响类中的任何其他有效代码,因为只有无效代码和样板文件调用_getZ。]

答案 4 :(得分:18)

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>
void as_mutable(T const &&) = delete;

现在你甚至无法搞砸volatile,用法看起来像是

T & f() {
    return as_mutable(std::as_const(*this).f());
}

答案 5 :(得分:16)

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

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

我没有考虑此解决方案的所有可能(负面)含义。如果有的话请告诉我。

答案 6 :(得分:6)

您也可以使用模板解决此问题。这个解决方案稍微有些丑陋(但是丑陋隐藏在.cpp文件中)但它确实提供了constness的编译器检查,并且没有代码重复。

.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不需要的包含。]

答案 7 :(得分:3)

如何将逻辑移动到私有方法中,并且仅在getter中执行“获取引用和返回”内容?实际上,我会对一个简单的getter函数中的static和const转换相当困惑,除了非常罕见的情况之外我会认为它很丑陋!

答案 8 :(得分:1)

我为一位合理使用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;
}

答案 9 :(得分:1)

虽然这里的大多数答案都建议使用 const_cast,但 CppCoreGuidelines 对此有一个 section

<块引用>

相反,更喜欢共享实现。通常,您可以让非常量函数调用 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。这不使用任何 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 的全部或部分可以是非依赖的,并且可以分解为一个通用的非模板函数,那么进一步的改进将是代码大小的潜在大幅减少。

答案 10 :(得分:1)

这是我想出的:

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

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

MAYBE_CONST的参数重复。在第一个副本中,CV被替换为空;并在第二个副本中将其替换为const

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


实施:

#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

答案 11 :(得分:1)

令我惊讶的是,有这么多不同的答案,但几乎所有人都依赖繁琐的模板魔术。模板功能强大,但有时宏会简洁地击败它们。通常,通过将两者结合在一起可以实现最大的多功能性。

我写了一个宏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()中,这可以推断其参数类型并删除co​​nst限定符。其次,可以在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版本,则可以考虑其他方法。

答案 12 :(得分:1)

对于那些像我一样的人

  • 使用 c ++ 17
  • 要添加最少样板 /重复和
  • 不要介意使用 makros (正在等待元类...),

这是另一种方法:

#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(a...))>::type {  \
        return const_cast<decltype(func(a...))>(                            \
            std::as_const(*this).func(std::forward<T>(a)...));              \
    }

这基本上是@ Pait,@ DavidStone和@ sh1的答案的混合。它添加到表中的是,您只需要多一行代码即可简单地命名该函数(而无需参数或返回类型重复项):

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

注意:gcc无法在8.1之前编译此版本,clang-5及更高版本以及MSVC-19都很满意(根据the compiler explorer)。

答案 13 :(得分:1)

使用预处理器是否作弊?

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

};

它不像模板或演员表那样华丽,但它确实使你的意图(“这两个函数必须完全相同”)非常明确。

答案 14 :(得分:0)

没找到我要找的东西,所以我自己动了几个......

这个有点罗嗦,但有一个优点就是同时处理许多同名(和返回类型)的重载方法:

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方法的引用参数无法与模板的明显的by-value参数匹配,并且它会中断。 不确定原因。 Here's why

答案 15 :(得分:0)

我建议使用私有帮助程序静态函数模板,如下所示:

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

答案 16 :(得分:0)

要添加到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;
};

答案 17 :(得分:0)

通常,您需要const和非const版本的成员函数是getter和setter。大部分时间它们都是单行的,因此代码重复不是问题。

答案 18 :(得分:0)

这是模板静态帮助程序函数的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

答案 19 :(得分:-1)

This DDJ article显示了一种使用模板特化的方法,不需要使用const_cast。对于这样一个简单的功能,它确实不需要。

boost :: any_cast(在某一点上,它不再存在)使用const版本中的const_cast来调用非const版本以避免重复。你不能在非const版本上强加const语义,所以你必须非常小心。

最后,只要两个代码段直接相互叠加,一些代码重复 即可。

相关问题