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

How do I remove code duplication between similar const and non-const member functions?

提问人:Kevin 提问时间:9/24/2008 最后编辑:Jan SchultkeKevin 更新时间:10/9/2023 访问量:55735

问:

假设我有以下情况,我想将访问权限返回给内部成员: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

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

C 代码复制 C++-FAQ 函数限定符 显式对象参数

评论

0赞 Matt Price 9/24/2008
在此示例中,我将返回一个 const 情况下的值,因此您不能在下面进行重构。int Z() const { return z;
2赞 Kevin 9/24/2008
对于基本类型,你是绝对正确的!我的第一个例子不是很好。假设我们返回的是某个类实例。(我更新了问题以反映这一点。

答:

74赞 9 revs, 4 users 76%Kevin #1

是的,可以避免代码重复。您需要使用 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
};

注意:重要的是,不要将逻辑放在非常量函数中,而让常量函数调用非常量函数 - 这可能会导致未定义的行为。原因是常量类实例被强制转换为非常量实例。非常量成员函数可能会意外修改类,C++ 标准状态将导致未定义的行为。

评论

5赞 Shog9 9/24/2008
哇。。。太可怕了。你只是增加了代码量,降低了清晰度,并增加了两个臭const_cast<>。也许你有一个例子,这实际上是有意义的?
17赞 jwfearn 9/24/2008
嘿,别说这个!,它可能很丑陋,但根据斯科特·迈耶斯(Scott Meyers)的说法,这(几乎)是正确的方法。参见“避免常量和非成本成员函数中的重复”标题下的有效 C++,3d 版,第 3 项。
21赞 Kevin 9/24/2008
虽然我知道解决方案可能很丑陋,但想象一下,确定要返回的内容的代码有 50 行长。那么重复是非常不可取的——尤其是当你必须重构代码时。在我的职业生涯中,我遇到过很多次这种情况。
10赞 Steve Jessop 9/24/2008
this 和 Meyers 的区别在于 Meyers 有static_cast<const X&>(*this)。const_cast用于删除 const,而不是添加它。
13赞 Caleth 3/15/2017
@VioletGiraffe我们知道该对象最初不是 const 创建的,因为它是非 const 对象的非常量成员,我们知道这一点是因为我们处于所述对象的非常量方法中。编译器不进行这种推断,它遵循保守的规则。如果不是这种情况,你为什么认为const_cast存在?
-2赞 Greg Rogers 9/24/2008 #2

这篇 DDJ 文章介绍了一种使用模板专用化的方法,该方法不需要使用const_cast。对于如此简单的功能,它确实不需要。

boost::any_cast(在某一时刻,它不再使用)使用 const 版本中的const_cast调用非 const 版本以避免重复。但是,您不能将 const 语义强加给 non-const 版本,因此您必须非常小心。

最后,只要两个代码段直接在彼此之上,一些代码重复可以的。

评论

0赞 Kevin 9/24/2008
DDJ 的文章似乎提到了迭代器——这与问题无关。常量迭代器不是常量数据,它们是指向常量数据的迭代器。
-1赞 Dima 9/24/2008 #3

通常,需要 const 和非 const 版本的成员函数是 getter 和 setter。大多数情况下,它们是单行代码,因此代码重复不是问题。

评论

2赞 Kevin 9/24/2008
大多数时候可能是真的。但也有例外。
1赞 jwfearn 9/24/2008
无论如何,恒量二传手没有多大意义;)
0赞 Dima 9/24/2008
我的意思是,非常量 getter 实际上是一个 setter。:)
246赞 jwfearn 9/24/2008 #4

有关详细说明,请参阅第 23 页第 3 项“尽可能使用”中的标题“避免重复和非成员功能”,载于 Scott Meyers 的 Effective C++,3d ed,ISBN-13:9780321334879。constconstconst

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

两个强制转换和函数调用可能很丑陋,但在非方法中是正确的,因为这意味着对象不是一开始就。(迈耶斯对此进行了深入的讨论。constconst

评论

64赞 Steve Jessop 9/24/2008
没有人因为追随斯科特·迈耶斯而被解雇:-)
12赞 Steve Jessop 9/24/2008
维特坎普说得对,一般来说,使用const_cast是不好的。正如 Meyers 所解释的那样,这是一个特殊情况,事实并非如此。@Adam:ROM => const 就可以了。const == ROM 显然是无稽之谈,因为任何人都可以随意将非 const 转换为 const-nilly:这相当于选择不修改某些东西。
49赞 Greg Rogers 11/23/2008
一般来说,我建议使用 const_cast 而不是 static_cast 来添加 const,因为它可以防止您意外更改类型。
11赞 Steve Jessop 4/5/2013
@HelloGoodbye:我认为 Meyers 从类接口的设计者那里获得了一点智慧。如果返回定义为 const 对象的内容,则根本不应该有 const 版本。实际上,随着时间的推移,我对此的想法发生了变化:模板解决方案是避免重复并获得编译器检查常量正确性的唯一方法,因此就我个人而言,为了避免重复代码,我会选择将复制的代码放入函数模板中,或者让它被欺骗。get()constget()const_cast
18赞 Deduplicator 3/19/2019
@CaseyRodarmor 现在使用 C++17 std::as_const() 更好
24赞 Steve Jessop 9/24/2008 #5

比 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 实例返回一个非常量 Z&,这就是它是私有的原因。私有方法可能会破坏外部接口的不变量(在这种情况下,所需的不变量是“不能通过通过它获得的对它所具有的对象的引用来修改 const 对象”)。

请注意,注释是模式的一部分 - _getZ 的接口指定调用它永远无效(显然,除了访问器):无论如何,这样做没有任何好处,因为它要键入的字符增加了 1 个字符,并且不会导致更小或更快的代码。调用该方法等效于使用const_cast调用其中一个访问器,您也不想这样做。如果你担心错误变得明显(这是一个公平的目标),那么就称之为const_cast_getZ而不是_getZ。

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

[编辑:Kevin 正确地指出,_getZ可能想要调用另一个方法(比如 generateZ),该方法与 getZ 相同。在这种情况下,_getZ将看到一个常量 Z&,并且必须在返回之前const_cast它。这仍然是安全的,因为样板访问器会监管所有内容,但它的安全并不是很明显。此外,如果你这样做了,然后把 generateZ 改成总是返回 const,那么你还需要把 getZ 改成总是返回 const,但编译器不会告诉你这样做。

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

评论

5赞 Kevin 9/24/2008
这仍然存在一个问题,即对于 X 的常量实例,您返回的东西可能是恒定的。在这种情况下,您仍然需要_getZ(...)中的const_cast。如果被后来的开发人员滥用,它仍然可能导致 UB。如果返回的东西是“可变的”,那么这是一个很好的解决方案。
1赞 Steve Jessop 9/24/2008
任何私有函数(哎呀,公共函数)都可能被后来的开发人员滥用,如果他们选择忽略 BLOCK CAPITAL 关于其有效使用的说明,在头文件和 Doxygen 等中也是如此。我无法阻止这种情况,我不认为这是我的问题,因为说明很容易理解。
16赞 Gravity 8/5/2011
-1:这在许多情况下不起作用。如果函数中有一个实例变量怎么办?编译器(或至少某些编译器)会抱怨说,既然是 const,那么其中引用的任何实例变量也是 const。因此,它将是 const(它将是 类型 ),并且无法转换为 。根据我的经验(诚然有些有限),在这种情况下,大多数时候都是实例变量。something_getZ()_getZ()somethingconst Z&Z&something
2赞 Steve Jessop 8/5/2011
@GravityBringer:那么“某物”需要涉及一个.它旨在成为从 const 对象获取非常量返回所需的代码的占位符,而不是作为重复 getter 中的内容的占位符。因此,“某物”不仅仅是一个实例变量。const_cast
2赞 Gravity 8/7/2011
明白了。不过,这确实降低了该技术的实用性。我会删除反对票,但 SO 不会让我。
3赞 MP24 9/24/2008 #6

将逻辑移动到私有方法中,并且只在 getter 中执行“获取引用和返回”的东西怎么样?实际上,我会对一个简单的 getter 函数中的 static 和 const 强制转换感到困惑,除非在极少数情况下,否则我认为这很丑陋!

评论

0赞 Kevin 9/24/2008
为了避免未定义的行为,您仍然需要一个const_cast。请参阅马丁·约克(Martin York)的答案和我的评论。
1赞 Peter Nimmo 3/4/2011
凯文,马丁·约克的答案是什么
8赞 Andy Balaam 1/14/2009 #7

您也可以使用模板来解决这个问题。这个解决方案有点丑陋(但丑陋隐藏在 .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 的成员(其中始终需要常量和非恒量版本),要么您可以将此函数设为朋友。但我不喜欢朋友。

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

评论

4赞 CB Bailey 1/14/2009
您始终可以将复杂的实现函数设置为静态成员,以获取对私有成员的访问权限。函数只需要在类头文件中声明,定义可以驻留在类实现文件中。毕竟,它是类实现的一部分。
0赞 Andy Balaam 1/14/2009
啊,是的,好主意!我不喜欢标题中出现的模板内容,但如果由于这里它可能使实现变得相当简单,那么它可能是值得的。
0赞 HelloGoodbye 4/5/2013
+ 1 到这个解决方案,它不重复任何代码,也不使用任何丑陋的代码(它可能意外地用于 canst 实际上应该是 const 的东西,而不是 const)。const_cast
0赞 Davis Herring 3/19/2020
如今,这可以通过模板的推导返回类型来简化(特别有用,因为它减少了成员情况下类中必须重复的内容)。
37赞 Pait 5/28/2013 #8

我认为Scott Meyers的解决方案可以通过使用tempate帮助函数在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赞 TBBle 2/4/2016
很遗憾我们不必去.std::remove_bottom_conststd::remove_const
0赞 v.oddou 3/4/2016
我不喜欢这个解决方案,因为它仍然嵌入了一个.你可以自己制作一个模板,并将里面类型的特征用于你需要的类型,如果需要,比如 s 或 s。真正的问题是,当这部分签名无法模板化时,如何生成方法的常量版本?const_castgetElementmpl::conditionaliteratorconstiterator
2赞 TBBle 3/4/2016
@v.oddou:是(去掉顶级资格),因此这个答案中的体操。推定可以删除底层限定条件,并准确地执行此处的作用: => 。std::remove_const<int const&>int const &constNonConst<T>std::remove_bottom_constconstNonConst<T>std::remove_bottom_const<int const&>::typeint&
4赞 John 6/20/2016
如果重载,此解决方案效果不佳。然后,如果不显式提供模板参数,就无法解析函数指针。为什么?getElement
1赞 ShaulF 5/19/2017
您需要修复您的答案才能使用 C++11 完美转发: 完成: gist.github.com/BlueSolei/bca26a8590265492e2f2760d3cefcf83likeConstVersion(TObj const* obj, TConstReturn (TObj::*memFun)(TArgs...) const, TArgs&&... args) { return const_cast<typename NonConst<TConstReturn>::type>((obj->*memFun)(std::forward<TArgs>(args)...)); }
33赞 gd1 12/8/2013 #9

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

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赞 kfsone 4/28/2015
好吧,让我们继续一个简单的事实,即您添加了更多样板。如果有的话,这应该用作为什么语言需要一种方法来修改函数限定符以及返回类型。为什么是“&&”?啊,所以我可以说:auto get(std::size_t i) -> auto(const), auto(&&)auto foo() -> auto(const), auto(&&) = delete;
1赞 v.oddou 3/4/2016
@kfsone语法应该是 Incorporated 关键字。我建议将 this 关键字识别为隐式对象实例参数,并让编译器识别 myfunction 是成员或 . 将在呼叫站点上自动推断,该站点将始终是课程的类型,但具有免费的简历资格。thistemplate< typename T > auto myfunction(T this, t args) -> decltype(ident)TT
4赞 Jarod42 8/8/2017
该解决方案还具有允许返回和 .const_castiteratorconst_iterator
1赞 Jarod42 8/8/2017
如果在 cpp 文件中移动实现(并且由于不重复的方法不应该是微不足道的,因此可能是这种情况),可以在文件范围而不是类范围完成。:-)static
1赞 iPherian 1/16/2020
我最喜欢这个解决方案。它避免了许多隐藏的陷阱。聪明可以通过常量投射获得 99% 的安全性,但也有一些边缘情况潜伏在阴影中。
-1赞 Christer Swahn 11/20/2014 #10

为了添加到 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;
};
0赞 matovitch 6/16/2015 #11

我这样做是为了一个朋友,他理所当然地证明了使用......不知道我可能会做这样的事情(不是很优雅):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;
}
2赞 dats 10/7/2015 #12

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

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);
    }
};
-1赞 sh1 12/15/2016 #13

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

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

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

2赞 user1476176 1/14/2017 #14

使用预处理器是作弊吗?

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

};

它不像模板或演员表那样花哨,但它确实使你的意图(“这两个功能是相同的”)非常明确。

评论

2赞 Ruslan 1/23/2017
但是,您必须小心使用反斜杠(就像多行宏一样),此外,在大多数(如果不是全部)编辑器中,您都会丢失语法突出显示。
99赞 David Stone 11/19/2017 #15

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

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

这样做的优点是:

  • 很明显发生了什么
  • 具有最小的代码开销 -- 它适合一行
  • 很难出错(只能偶然抛弃,但是一个罕见的限定词)volatilevolatile

如果你想走完整的演绎路线,那么这可以通过一个辅助函数来实现

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

评论

0赞 Max Truxa 7/25/2018
请注意,删除了 const rvalue 重载的 “as_mutable” (通常更可取) 会阻止最后一个示例工作 if 返回而不是 .f()TT&
3赞 David Stone 7/26/2018
@MaxTruxa:是的,这是一件好事。如果它只是编译,我们将有一个悬空的引用。在 return 的情况下,我们不希望有两个重载,仅版本就足够了。f()Tconst
0赞 Max Truxa 7/26/2018
没错,我为我昨天的脑子放屁道歉,不知道我写那条评论时在想什么。我正在查看一个 const/mutable getter 对返回一个 .所以我真正需要的是看起来与上面几乎相同的东西,只是它接受并返回 a 和 uses 而不是 .shared_ptras_mutable_ptras_mutableshared_ptrstd::const_pointer_castconst_cast
1赞 monkey0506 8/12/2019
如果一个方法返回,那么这将绑定到而不是绑定到(至少在我的测试中是这样)。我必须添加一个重载作为返回指针的方法的参数类型。T const*T const* const&&T const* const&T const*
3赞 David Stone 9/10/2019
@monkey0506:我已经更新了我的答案以支持指针和参考资料
8赞 axxel 3/30/2019 #16

对于那些(像我一样)的人

  • 使用 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 12/16/2019
这对我来说是直接有效的。这是一个很好的答案,谢谢!
0赞 cdhowie 5/21/2020
难道不应该在参数上使用 s 来确保我们在采用不同类型的引用的重载的情况下使用正确的返回类型吗?decltype()std::forwardget()
0赞 axxel 5/21/2020
@cdhowie 您能举个例子吗?
0赞 cdhowie 5/22/2020
@axxel 这是人为的,但你去吧。由于类型中缺少转发,宏错误地推断返回类型,并将 s 推断为错误的类型。用它们代替它们可以解决这个问题。(只有一个链接器错误,因为我从未定义过任何声明的重载。NON_CONSTconst_castdecltype(func(a...))decltype(func(std::forward<T>(a)...))X::get
1赞 axxel 5/22/2020
谢谢@cdhowie,我拉皮条你的例子实际上使用了非常量重载:coliru.stacked-crooked.com/a/0cedc7f4e789479e
2赞 TheOperator 6/21/2019 #17

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

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

用法示例:

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)

解释:

正如许多答案中所发布的,避免在非常量成员函数中重复代码的典型模式是这样的:

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

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

剩下的就是一个简单的宏,它用正确的限定值作为调用的前缀,并从结果中删除 const。由于宏中使用的表达式几乎总是具有 1:1 转发参数的简单函数调用,因此宏的缺点(例如多重计算)不会发挥作用。也可以使用省略号,但不需要,因为逗号(作为参数分隔符)出现在括号内。this->__VA_ARGS__

此方法有几个优点:

  • 最小而自然的语法 -- 只需将调用包装起来FROM_CONST_OVERLOAD( )
  • 无需额外的成员功能
  • 与 C++98 兼容
  • 实现简单,无模板元编程,零依赖
  • 可扩展:可以添加其他常量关系(如、等)。为此,只需重载相应的类型即可。const_iteratorstd::shared_ptr<const T>WithoutConst()

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

1赞 HolyBlackCat 10/20/2019 #18

我想出了一个宏,可以自动生成成对的常量/非常量函数。

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

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

的参数是重复的。在第一个副本中,被替换为 nothing;在第二个副本中,它被替换为 .MAYBE_CONSTCVconst

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

不过有一点不便。如果出现在括号内,则此对括号必须以 :CVCV_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)

不支持的 C++ 之前的实现: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
6赞 atablash 12/6/2019 #19

如果您不喜欢常量转换,我使用另一个答案建议的 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

5赞 ivaigult 6/23/2021 #20

虽然这里的大多数答案都建议使用 ,但 CppCoreGuidelines 有一是关于这一点的:const_cast

相反,更喜欢共享实现。通常,您可以让非常量函数调用常量函数。但是,当存在复杂的逻辑时,这可能会导致以下模式仍诉诸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可以不依赖,并分解为一个通用的非模板函数,则代码大小可能会大幅减少。

33赞 David Stone 10/8/2021 #21

由于显式对象参数,C++23 更新了这个问题的最佳答案。

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

单个函数模板可作为普通成员函数调用,并为您推断正确的引用类型。没有出错的投射,没有为概念上是一回事的东西编写多个函数。


注意:此功能由 P0847 添加:推断此功能。

评论

2赞 Jan Schultke 10/9/2023
值得注意的是,此成员函数还接受限定对象,并且它接受表达式的任何值类别。这使得它比 /non- 对函数更强大。volatileconstconst