如何删除类似 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
大括号内有相同的代码。 这是重复的代码并且可能会导致具有复杂逻辑的长函数的维护问题。
有没有办法避免这种代码重复?
Let's say I have the following class X
where I want to return access to an internal member:
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
}
};
The two member functions X::Z()
and X::Z() const
have identical code inside the braces. This is duplicate code and can cause maintenance problems for long functions with complex logic.
Is there a way to avoid this code duplication?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(22)
我想出了一个可以自动生成常量/非常量函数对的宏。
具体实现请见答案末尾。
MAYBE_CONST
的参数重复。 在第一个副本中,CV
被替换为任何内容; 在第二个副本中,它被替换为 const。CV
在宏参数中出现的次数没有限制。不过还是有一点不便。 如果
CV
出现在括号内,则这对括号必须以CV_IN
为前缀:实现:
不支持
CV_IN< 的 Pre-C++20 实现/代码>:
I came up with a macro that generates pairs of const/non-const functions automatically.
See the end of the answer for the implementation.
The argument of
MAYBE_CONST
is duplicated. In the first copy,CV
is replaced with nothing; and in the second copy it's replaced withconst
.There's no limit on how many times
CV
can appear in the macro argument.There's a slight inconvenience though. If
CV
appears inside of parentheses, this pair of parentheses must be prefixed withCV_IN
:Implementation:
Pre-C++20 implementation that doesn't support
CV_IN
:我为一位朋友做了这件事,他正确地证明了使用 const_cast 的合理性...如果不知道这一点,我可能会做这样的事情(不是很优雅):
I did this for a friend who rightfully justified the use of
const_cast
... not knowing about it I probably would have done something like this (not really elegant) :通常,需要 const 和非 const 版本的成员函数是 getter 和 setter。 大多数时候它们都是单行代码,因此代码重复不是问题。
Typically, the member functions for which you need const and non-const versions are getters and setters. Most of the time they are one-liners so code duplication is not an issue.
添加到jwfearn和kevin提供的解决方案中,下面是函数返回shared_ptr时对应的解决方案:
To add to the solution jwfearn and kevin provided, here's the corresponding solution when the function returns shared_ptr:
没有找到我要找的东西,所以我自己推出了几个...
这个有点罗嗦,但具有一次性处理许多同名(和返回类型)的重载方法的优点:
如果每个名称只有一个
const
方法,但仍然有很多方法需要重复,那么您可能更喜欢这样:不幸的是,一旦您开始重载名称(函数指针参数的参数列表),这种情况就会崩溃此时似乎尚未解决,因此无法找到函数参数的匹配项)。 尽管您也可以通过模板来解决这个问题:
但是
const
方法的引用参数无法与模板的明显按值参数匹配,并且它会中断。不知道为什么。原因如下。Didn't find what I was looking for, so I rolled a couple of my own...
This one is a little wordy, but has the advantage of handling many overloaded methods of the same name (and return type) all at once:
If you have only one
const
method per name, but still plenty of methods to duplicate, then you might prefer this:Unfortunately this breaks down as soon as you start overloading the name (the function pointer argument's argument list seems to be unresolved at that point, so it can't find a match for the function argument). Although you can template your way out of that, too:
But reference arguments to the
const
method fail to match against the apparently by-value arguments to the template and it breaks.Not sure why.Here's why.如何将逻辑转移到私有方法中,并且只在 getters 中执行“获取引用并返回”的操作? 实际上,我会对简单 getter 函数内的 static 和 const 强制转换感到相当困惑,并且我认为除了极少数情况外,这很丑陋!
How about moving the logic into a private method, and only doing the "get the reference and return" stuff inside the getters? Actually, I would be fairly confused about the static and const casts inside a simple getter function, and I'd consider that ugly except for extremely rare circumstances!
我建议使用私有辅助静态函数模板,如下所示:
I'd suggest a private helper static function template, like this:
使用预处理器是作弊吗?
它不像模板或强制转换那么花哨,但它确实使您的意图(“这两个函数是相同的”)非常明确。
Is it cheating to use the preprocessor?
It's not as fancy as templates or casts, but it does make your intent ("these two functions are to be identical") pretty explicit.
令我惊讶的是,有这么多不同的答案,但几乎所有答案都依赖于沉重的模板魔法。 模板很强大,但有时宏在简洁性方面胜过它们。 最大的多功能性通常是通过将两者结合起来实现的。
我编写了一个宏
FROM_CONST_OVERLOAD()
,可以将其放置在非常量函数中以调用const函数。用法示例:
简单且可重用的实现:
说明:
正如许多答案中所发布的那样,避免非常量成员函数中的代码重复的典型模式是这样的:
使用类型推断可以避免很多此类样板文件。 首先,
const_cast
可以封装在WithoutConst()
中,它会推断其参数的类型并删除 const 限定符。 其次,可以在WithConst()
中使用类似的方法来对this
指针进行 const 限定,从而可以调用 const 重载方法。其余部分是一个简单的宏,它在调用前加上正确限定的
this->
前缀,并从结果中删除 const。 由于宏中使用的表达式几乎总是带有 1:1 转发参数的简单函数调用,因此不会出现宏的缺点(例如多重求值)。也可以使用省略号和 __VA_ARGS__,但是不需要,因为逗号(作为参数分隔符)出现在括号内。这种方法有几个好处:
FROM_CONST_OVERLOAD( )
中WithoutConst()
即可。限制:此解决方案针对非常量重载与常量重载完全相同的场景进行了优化,因此参数可以按 1:1 转发。 如果您的逻辑不同并且您没有通过
this->Method(args)
调用 const 版本,您可以考虑其他方法。It's surprising to me that there are so many different answers, yet almost all rely on heavy template magic. Templates are powerful, but sometimes macros beat them in conciseness. Maximum versatility is often achieved by combining both.
I wrote a macro
FROM_CONST_OVERLOAD()
which can be placed in the non-const function to invoke the const function.Example usage:
Simple and reusable implementation:
Explanation:
As posted in many answers, the typical pattern to avoid code duplication in a non-const member function is this:
A lot of this boilerplate can be avoided using type inference. First,
const_cast
can be encapsulated inWithoutConst()
, which infers the type of its argument and removes the const-qualifier. Second, a similar approach can be used inWithConst()
to const-qualify thethis
pointer, which enables calling the const-overloaded method.The rest is a simple macro that prefixes the call with the correctly qualified
this->
and removes const from the result. Since the expression used in the macro is almost always a simple function call with 1:1 forwarded arguments, drawbacks of macros such as multiple evaluation do not kick in. The ellipsis and__VA_ARGS__
could also be used, but should not be needed because commas (as argument separators) occur within parentheses.This approach has several benefits:
FROM_CONST_OVERLOAD( )
const_iterator
,std::shared_ptr<const T>
, etc.). For this, simply overloadWithoutConst()
for the corresponding types.Limitations: this solution is optimized for scenarios where the non-const overload is doing exactly the same as the const overload, so that arguments can be forwarded 1:1. If your logic differs and you are not calling the const version via
this->Method(args)
, you may consider other approaches.我做了一个宏
https://gist.github.com/esnosy/947565a4e651991dc6a25f3e9f467285
I made a macro
https://gist.github.com/esnosy/947565a4e651991dc6a25f3e9f467285
这篇 DDJ 文章展示了一种使用模板专业化的方法,不需要使用 const_cast。 对于这样一个简单的功能,它实际上是不需要的。
boost::any_cast (在某一时刻,它不再是了)使用 const 版本中的 const_cast 调用非 const 版本来避免重复。 不过,您不能将 const 语义强加于非常量版本,因此您必须对此非常小心。
最后,只要两个片段直接重叠,一些代码重复就可以了。
This DDJ article shows a way using template specialization that doesn't require you to use const_cast. For such a simple function it really isn't needed though.
boost::any_cast (at one point, it doesn't any more) uses a const_cast from the const version calling the non-const version to avoid duplication. You can't impose const semantics on the non-const version though so you have to be very careful with that.
In the end some code duplication is okay as long as the two snippets are directly on top of each other.
有关详细说明,请参阅标题“避免在
const
和非const
成员函数中出现重复”,第 144 页。 23,在 Effective C++ 中的第 3 项“尽可能使用const
” ,3d 编辑,作者:Scott Meyers,ISBN-13:9780321334879。这是 Meyers 的解决方案(简化):
两个强制转换和函数调用可能很难看,但在非 const 方法中是正确的,因为这意味着该对象不是首先是
const
。 (迈耶斯对此进行了深入的讨论。)For a detailed explanation, please see the heading "Avoid Duplication in
const
and Non-const
Member Function," on p. 23, in Item 3 "Useconst
whenever possible," in Effective C++, 3d ed by Scott Meyers, ISBN-13: 9780321334879.Here's Meyers' solution (simplified):
The two casts and function call may be ugly, but it's correct in a non-
const
method as that implies the object was notconst
to begin with. (Meyers has a thorough discussion of this.)C++17 更新了这个问题的最佳答案:
这有以下优点:
易失性偶然,但<代码>易失性是一个罕见的限定符)
如果你想走完整的推导路线那么可以通过有一个辅助函数来完成
现在你甚至不能搞砸
易失性
,用法如下C++17 has updated the best answer for this question:
This has the advantages that it:
volatile
by accident, butvolatile
is a rare qualifier)If you want to go the full deduction route then that can be accomplished by having a helper function
Now you can't even mess up
volatile
, and the usage looks like是的,可以避免代码重复。 您需要使用 const 成员函数来具有逻辑,并让非常量成员函数调用 const 成员函数并将返回值重新转换为非常量引用(如果函数返回指针,则为指针):
< strong>注意:重要的是,您不要将逻辑放入非常量函数中并让 const 函数调用非常量函数 - 这可能会导致未定义行为。 原因是常量类实例被转换为非常量实例。 非常量成员函数可能会意外修改类,C++ 标准规定这将导致未定义的行为。
Yes, it is possible to avoid the code duplication. You need to use the const member function to have the logic and have the non-const member function call the const member function and re-cast the return value to a non-const reference (or pointer if the functions returns a pointer):
NOTE: It is important that you do NOT put the logic in the non-const function and have the const-function call the non-const function -- it may result in undefined behavior. The reason is that a constant class instance gets cast as a non-constant instance. The non-const member function may accidentally modify the class, which the C++ standard states will result in undefined behavior.
由于 显式对象参数。
单个函数模板可作为普通成员函数进行调用,并为您推导出正确的引用类型。 不会出现错误的转换,也不会为概念上是一件事的东西编写多个函数。
注意:此功能是由P0847:推论添加的。
C++23 has updated the best answer for this question thanks to explicit object parameters.
A single function template is callable as a normal member function and deduces the correct reference type for you. No casting to get wrong, no writing multiple functions for something that is conceptually one thing.
Note: this feature was added by P0847: Deducing this.
我认为 Scott Meyers 的解决方案可以通过使用模板辅助函数在 C++11 中得到改进。 这使得意图更加明显,并且可以被许多其他 getter 重用。
该辅助函数可以通过以下方式使用。
第一个参数始终是 this 指针。 第二个是指向要调用的成员函数的指针。 之后可以传递任意数量的附加参数,以便将它们转发给函数。
由于可变参数模板,这需要 C++11。
I think Scott Meyers' solution can be improved in C++11 by using a tempate helper function. This makes the intent much more obvious and can be reused for many other getters.
This helper function can be used the following way.
The first argument is always the this-pointer. The second is the pointer to the member function to call. After that an arbitrary amount of additional arguments can be passed so that they can be forwarded to the function.
This needs C++11 because of the variadic templates.
比 Meyers 更冗长一些,但我可能会这样做:
私有方法有一个不受欢迎的属性,即它返回一个非常量 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 的模式失去了很多价值。 由于与迈耶斯相比它也有缺点,我想在这种情况下我会改用他的。 从一种重构到另一种很容易——它不会影响类中的任何其他有效代码,因为只有无效代码和样板文件调用 _getZ。]
A bit more verbose than Meyers, but I might do this:
The private method has the undesirable property that it returns a non-const Z& for a const instance, which is why it's private. Private methods may break invariants of the external interface (in this case the desired invariant is "a const object cannot be modified via references obtained through it to objects it has-a").
Note that the comments are part of the pattern - _getZ's interface specifies that it is never valid to call it (aside from the accessors, obviously): there's no conceivable benefit to doing so anyway, because it's 1 more character to type and won't result in smaller or faster code. Calling the method is equivalent to calling one of the accessors with a const_cast, and you wouldn't want to do that either. If you're worried about making errors obvious (and that's a fair goal), then call it const_cast_getZ instead of _getZ.
By the way, I appreciate Meyers's solution. I have no philosophical objection to it. Personally, though, I prefer a tiny bit of controlled repetition, and a private method that must only be called in certain tightly-controlled circumstances, over a method that looks like line noise. Pick your poison and stick with it.
[Edit: Kevin has rightly pointed out that _getZ might want to call a further method (say generateZ) which is const-specialised in the same way getZ is. In this case, _getZ would see a const Z& and have to const_cast it before return. That's still safe, since the boilerplate accessor polices everything, but it's not outstandingly obvious that it's safe. Furthermore, if you do that and then later change generateZ to always return const, then you also need to change getZ to always return const, but the compiler won't tell you that you do.
That latter point about the compiler is also true of Meyers's recommended pattern, but the first point about a non-obvious const_cast isn't. So on balance I think that if _getZ turns out to need a const_cast for its return value, then this pattern loses a lot of its value over Meyers's. Since it also suffers disadvantages compared to Meyers's, I think I would switch to his in that situation. Refactoring from one to the other is easy -- it doesn't affect any other valid code in the class, since only invalid code and the boilerplate calls _getZ.]
好问题和好答案。 我有另一个解决方案,不使用强制转换:
但是,它的丑陋之处在于需要静态成员并且需要在其中使用
instance
变量。我没有考虑该解决方案的所有可能(负面)影响。 如果有的话请告诉我。
Nice question and nice answers. I have another solution, that uses no casts:
However, it has the ugliness of requiring a static member and the need of using the
instance
variable inside it.I did not consider all the possible (negative) implications of this solution. Please let me know if any.
如果您不喜欢 const 转换,我可以使用 另一个答案,带有可选的 SFINAE 测试。
完整版:https://godbolt.org/z/mMK4r3
If you don't like const casting, I use this C++17 version of the template static helper function suggested by another answer, with and optional SFINAE test.
Full version: https://godbolt.org/z/mMK4r3
虽然这里的大多数答案建议使用 const_cast ,但 CppCoreGuidelines 有一个 部分 关于这一点:
While most of answers here suggest to use a
const_cast
, CppCoreGuidelines have a section about that:的人(像我一样)
这是另一种做法:
它基本上是 @Pait、@DavidStone 和 @sh1 的答案的混合(编辑:以及 @cdhowie 的改进)。 它添加到表中的内容是,您只需要一行额外的代码,它只是简单地命名函数(但没有参数或返回类型重复):
注意:gcc 无法在 8.1、clang-5 及更高版本之前编译此代码,如下所示MSVC-19 很高兴(根据编译器资源管理器)。
For those (like me) who
here is another take:
It is basically a mix of the answers from @Pait, @DavidStone and @sh1 (EDIT: and an improvement from @cdhowie). What it adds to the table is that you get away with only one extra line of code which simply names the function (but no argument or return type duplication):
Note: gcc fails to compile this prior to 8.1, clang-5 and upwards as well as MSVC-19 are happy (according to the compiler explorer).
您也可以使用模板来解决这个问题。 这个解决方案有点丑陋(但丑陋隐藏在 .cpp 文件中),但它确实提供了编译器对常量的检查,并且没有代码重复。
.h 文件:
.cpp 文件:
我看到的主要缺点是,因为该方法的所有复杂实现都在全局函数中,所以您要么需要使用上面的 GetVector() 等公共方法来获取 X 的成员(其中总是需要有 const 和非 const 版本)或者你可以让这个函数成为一个朋友。 但我不喜欢朋友。
[编辑:删除了测试期间添加的不需要的 cstdio 包含内容。]
You could also solve this with templates. This solution is slightly ugly (but the ugliness is hidden in the .cpp file) but it does provide compiler checking of constness, and no code duplication.
.h file:
.cpp file:
The main disadvantage I can see is that because all the complex implementation of the method is in a global function, you either need to get hold of the members of X using public methods like GetVector() above (of which there always need to be a const and non-const version) or you could make this function a friend. But I don't like friends.
[Edit: removed unneeded include of cstdio added during testing.]