C++ 如何删除类似的 const 和非常量成员函数之间的代码重复?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/123758/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How do I remove code duplication between similar const and non-const member functions?
提问by Kevin
Let's say I have the following class X
where I want to return access to an internal member:
假设我有以下内容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
}
};
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.
这两个成员函数X::Z()
并X::Z() const
在大括号内具有相同的代码。这是重复的代码,可能会导致具有复杂逻辑的长函数的维护问题。
Is there a way to avoid this code duplication?
有没有办法避免这种代码重复?
采纳答案by Kevin
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):
是的,可以避免代码重复。您需要使用 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
};
NOTE:It is important that you do NOTput 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.
注意:重要的是不要将逻辑放在非常量函数中并且让常量函数调用非常量函数——这可能会导致未定义的行为。原因是常量类实例被转换为非常量实例。非常量成员函数可能会意外修改类,C++ 标准指出这将导致未定义的行为。
回答by jwfearn
For a detailed explanation, please see the heading "Avoid Duplication in const
and Non-const
Member Function," on p. 23, in Item 3 "Use const
whenever possible," in Effective C++, 3d edby Scott Meyers, ISBN-13: 9780321334879.
有关详细说明,请参阅第 10 页的标题“避免在const
非const
成员函数中重复”。23,在Effective C++中的第 3 项“const
尽可能使用”,Scott Meyers 3d 编辑,ISBN-13:9780321334879。
Here's Meyers' solution (simplified):
这是迈耶斯的解决方案(简化):
struct C {
const char & get() const {
return c;
}
char & get() {
return const_cast<char &>(static_cast<const C &>(*this).get());
}
char c;
};
The two casts and function call may be ugly but it's correct. Meyers has a thorough explanation why.
这两个转换和函数调用可能很难看,但它是正确的。迈耶斯有一个详尽的解释为什么。
回答by David Stone
C++17 has updated the best answer for this question:
C++17 更新了这个问题的最佳答案:
T const & f() const {
return something_complicated();
}
T & f() {
return const_cast<T &>(std::as_const(*this).f());
}
This has the advantages that it:
这具有以下优点:
- Is obvious what is going on
- Has minimal code overhead -- it fits in a single line
- Is hard to get wrong (can only cast away
volatile
by accident, butvolatile
is a rare qualifier)
- 很明显发生了什么
- 具有最小的代码开销——它适合在一行中
- 很难出错(只能
volatile
被偶然抛弃,但是volatile
是一个罕见的预选赛)
If you want to go the full deduction route then that can be accomplished by having a helper function
如果您想走完整的演绎路线,那么可以通过使用辅助功能来完成
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;
Now you can't even mess up volatile
, and the usage looks like
现在你甚至不能搞砸了volatile
,用法看起来像
decltype(auto) f() const {
return something_complicated();
}
decltype(auto) f() {
return as_mutable(std::as_const(*this).f());
}
回答by Pait
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.
我认为 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)...));
}
This helper function can be used the following way.
可以通过以下方式使用此辅助函数。
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);
}
};
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.
第一个参数始终是 this 指针。第二个是指向要调用的成员函数的指针。之后,可以传递任意数量的附加参数,以便将它们转发给函数。由于可变参数模板,这需要 C++11。
回答by gd1
Nice question and nice answers. I have another solution, that uses no casts:
很好的问题和很好的答案。我有另一个解决方案,它不使用强制转换:
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);
}
};
However, it has the ugliness of requiring a static member and the need of using the instance
variable inside it.
但是,它的丑陋之处在于需要一个静态成员,并且需要在其中使用instance
变量。
I did not consider all the possible (negative) implications of this solution. Please let me know if any.
我没有考虑这个解决方案的所有可能(负面)影响。如果有的话请告诉我。
回答by Steve Jessop
A bit more verbose than Meyers, but I might do this:
比迈耶斯更冗长,但我可能会这样做:
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); }
};
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").
私有方法有一个不受欢迎的特性,它为 const 实例返回一个非常量 Z& ,这就是它是私有的原因。私有方法可能会破坏外部接口的不变量(在这种情况下,所需的不变量是“不能通过通过它获得的对象引用来修改常量对象”)。
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.
请注意,注释是模式的一部分 - _getZ 的接口指定调用它永远无效(显然,除了访问器):无论如何这样做没有任何好处,因为它要输入 1 个字符并且不会导致更小或更快的代码。调用该方法等同于使用 const_cast 调用访问器之一,您也不想这样做。如果您担心使错误变得明显(这是一个公平的目标),那么将其称为 const_cast_getZ 而不是 _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.
[编辑:Kevin 正确地指出 _getZ 可能想要调用另一个方法(比如 generateZ),它与 getZ 的方式相同。在这种情况下, _getZ 会看到一个 const Z& 并且必须在返回之前对其进行 const_cast 。这仍然是安全的,因为样板访问器管理着一切,但它的安全性并不是很明显。此外,如果您这样做,然后将 generateZ 更改为始终返回 const,那么您还需要将 getZ 更改为始终返回 const,但编译器不会告诉您这样做。
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.]
关于编译器的后一点也适用于 Meyers 推荐的模式,但关于不明显的 const_cast 的第一点不是。所以总的来说,我认为如果 _getZ 需要一个 const_cast 作为它的返回值,那么这个模式比 Meyers 失去了很多价值。由于与迈耶斯相比,它也有劣势,我想在那种情况下我会转向他的。从一个重构到另一个很容易——它不会影响类中的任何其他有效代码,因为只有无效代码和样板调用 _getZ。]
回答by Andy Balaam
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.
你也可以用模板解决这个问题。这个解决方案有点难看(但丑陋隐藏在 .cpp 文件中),但它确实提供了编译器的常量性检查,并且没有代码重复。
.h file:
.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 file:
.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 );
}
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.
我能看到的主要缺点是,因为该方法的所有复杂实现都在一个全局函数中,您要么需要使用上面的 GetVector() 等公共方法来获取 X 的成员(其中总是需要一个const 和非常量版本)或者你可以让这个函数成为朋友。但我不喜欢朋友。
[Edit: removed unneeded include of cstdio added during testing.]
[编辑:删除了测试期间添加的 cstdio 的不需要的包含。]
回答by MP24
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!
如何将逻辑移动到私有方法中,并且只在 getter 中执行“获取引用并返回”的操作?实际上,我会对简单 getter 函数中的 static 和 const 强制转换感到相当困惑,并且我认为这很丑陋,除非是极少数情况!
回答by axxel
For those (like me) who
对于那些(像我一样)
- use c++17
- want to add the least amount of boilerplate/repetition and
- don't mind using macros(while waiting for meta-classes...),
- 使用C++17
- 想要添加最少的样板/重复和
- 不介意使用宏(在等待元类时......),
here is another take:
这是另一种看法:
#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)...)); \
}
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):
它基本上是@Pait、@DavidStone 和@sh1 的答案的混合(编辑:以及@cdhowie 的改进)。它添加到表中的是您只需要一行额外的代码就可以简单地命名函数(但没有参数或返回类型重复):
class X
{
const Z& get(size_t index) const { ... }
NON_CONST(get)
};
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).
注意:gcc 无法在 8.1 之前编译它,clang-5 及更高版本以及 MSVC-19 很高兴(根据编译器资源管理器)。
回答by TheOperator
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.
我写了一个宏FROM_CONST_OVERLOAD()
,它可以放在非常量函数中来调用 const 函数。
Example usage:
用法示例:
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) );
}
};
Simple and reusable implementation:
简单且可重用的实现:
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)
Explanation:
解释:
As posted in many answers, the typical pattern to avoid code duplication in a non-const member function is this:
正如许多答案中发布的那样,在非常量成员函数中避免代码重复的典型模式是这样的:
return const_cast<Result&>( static_cast<const MyClass*>(this)->Method(args) );
A lot of this boilerplate can be avoided using type inference. First, const_cast
can be encapsulated in WithoutConst()
, which infers the type of its argument and removes the const-qualifier. Second, a similar approach can be used in WithConst()
to const-qualify the this
pointer, which enables calling the const-overloaded method.
使用类型推断可以避免很多这种样板文件。首先,const_cast
可以封装在 中WithoutConst()
,它推断其参数的类型并删除 const 限定符。其次,可以使用类似的方法来WithConst()
对this
指针进行常量限定,从而可以调用常量重载方法。
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->
并从结果中删除 const。由于宏中使用的表达式几乎总是一个带有 1:1 转发参数的简单函数调用,因此不会出现宏的缺点,例如多重评估。省略号 和__VA_ARGS__
也可以使用,但不应该需要,因为逗号(如参数分隔符)出现在括号内。
This approach has several benefits:
这种方法有几个好处:
- Minimal and natural syntax -- just wrap the call in
FROM_CONST_OVERLOAD( )
- No extra member function required
- Compatible with C++98
- Simple implementation, no template metaprogramming and zero dependencies
- Extensible: other const relations can be added (like
const_iterator
,std::shared_ptr<const T>
, etc.). For this, simply overloadWithoutConst()
for the corresponding types.
- 最小和自然的语法——只需将调用包装起来
FROM_CONST_OVERLOAD( )
- 不需要额外的成员函数
- 与 C++98 兼容
- 实现简单,无模板元编程,零依赖
- 扩展性:其他常量关系可以添加(如
const_iterator
,std::shared_ptr<const T>
等)。为此,只需重载WithoutConst()
相应的类型。
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.
限制:此解决方案针对非常量重载与 const 重载完全相同的场景进行了优化,以便可以 1:1 转发参数。如果您的逻辑不同并且您没有通过 调用 const 版本this->Method(args)
,您可以考虑其他方法。