我经常发现自己必须定义一个函数的两个版本,以便有一个是const的,一个是非const的(通常是一个getter,但并不总是)。两者的不同之处仅在于一个的输入和输出是const,而另一个的输入和输出是非const。功能的内涵 - 真正的工作,是IDENTICAL。
然而,对于const-correctness,我需要他们两个。作为一个简单的实际示例,请采取以下措施:
inline const ITEMIDLIST * GetNextItem(const ITEMIDLIST * pidl)
{
return pidl ? reinterpret_cast<const ITEMIDLIST *>(reinterpret_cast<const BYTE *>(pidl) + pidl->mkid.cb) : NULL;
}
inline ITEMIDLIST * GetNextItem(ITEMIDLIST * pidl)
{
return pidl ? reinterpret_cast<ITEMIDLIST *>(reinterpret_cast<BYTE *>(pidl) + pidl->mkid.cb) : NULL;
}
如你所见,他们做同样的事情。我可以选择用另一个使用更多的演员来定义一个,如果胆量 - 实际工作不那么简单,那就更合适了:
inline const ITEMIDLIST * GetNextItem(const ITEMIDLIST * pidl)
{
return pidl ? reinterpret_cast<const ITEMIDLIST *>(reinterpret_cast<const BYTE *>(pidl) + pidl->mkid.cb) : NULL;
}
inline ITEMIDLIST * GetNextItem(ITEMIDLIST * pidl)
{
return const_cast<ITEMIDLIST *>(GetNextItem(const_cast<const ITEMIDLIST *>(pidl));
}
所以,我发现这非常繁琐且多余。但是如果我想编写const-correct代码,那么我要么必须提供上述两种代码,要么我必须使用const-casts丢弃我的“消费者代码”以解决仅定义一个或另一个的问题。
这有更好的模式吗?您认为这个问题的“最佳”方法是什么:
或者是否有更好的解决方法? 是否正在对语言本身进行工作以完全缓解或消除这个问题?
奖励积分:
编辑:
如果我只提供第一个 - 接受const返回const,那么任何需要修改返回项目的消费者,或者将返回的项目交给另一个将修改它的函数,必须抛弃const。
类似地,如果我只提供第二个定义 - 取非const并返回非const,那么具有const pidl的消费者必须抛弃constness才能使用上面的函数,老实说,这不是修改项目本身的常量。
可能需要更多抽象:
THING & Foo(THING & it);
const THING & Foo(const THING & it);
我希望有一个结构:
const_neutral THING & Foo(const_neutral THING & it);
我当然可以这样做:
THING & Foo(const THING & it);
但那总是以错误的方式揉搓我。我说的是“我不会修改你的内容,但是我会在你的代码中为你悄悄地托付给我的常量。”
现在,客户端有:
const THING & it = GetAConstThing();
...
ModifyAThing(Foo(it));
那是错的。 GetAConstThing与调用者的契约是给它一个const引用。调用者不应该修改东西 - 只对其使用const操作。是的,调用者可能是邪恶的,也是错误的,并抛弃了它的常量,但那只是邪恶(tm)。
对我而言,问题的关键在于Foo是中立的。它实际上并没有修改它给定的东西,但它的输出需要传播其参数的常量。
注意:第二次编辑格式化。
答案 0 :(得分:5)
IMO这是const系统的一个不幸的副产品,但它并不经常出现:只有当函数或方法给出某些东西的指针/引用时(无论它们是否修改某些东西,函数都可以' t分发它没有的权利或const-correctness会严重破坏,所以这些重载是不可避免的。)
通常情况下,如果这些功能只是一个短线,我只是重复它们。如果实现更复杂,我使用模板来避免代码重复:
namespace
{
//here T is intended to be either [int] or [const int]
//basically you can also assert at compile-time
//whether the type is what it is supposed to be
template <class T>
T* do_foo(T* p)
{
return p; //suppose this is something more complicated than that
}
}
int* foo(int* p)
{
return do_foo(p);
}
const int* foo(const int* p)
{
return do_foo(p);
}
int main()
{
int* p = 0;
const int* q = foo(p); //non-const version
foo(q); //const version
}
答案 1 :(得分:4)
这里真正的问题似乎是你向外界提供(相对)直接访问你班级内部的权利。在一些情况下(例如,容器类)可能有意义,但在大多数情况下,这意味着您提供对内部的低级访问作为哑数据,您应该在那里查看客户端代码<的更高级别的操作em> 使用该数据,然后直接从您的班级提供这些更高级别的操作。
编辑:虽然在这种情况下,显然没有涉及课程,但基本理念仍然相同。我不认为它也在推卸这个问题 - 我只是指出,虽然我同意 是一个问题,但只有这种情况很少发生。
我不确定低级代码是否也证明了这一点。我的大多数代码都比大多数人有很多理由使用它的级别要低得多,我仍然只是偶尔遇到它。
Edit2:我还应该提一下,C ++ 0x有一个auto
关键字的新定义,以及一个新的关键字(decltype
),这使得相当多的事情变得相当容易处理。我没有尝试用它们来实现这个确切的函数,但是这种一般的情况是他们想要的东西(例如,根据传递的参数自动计算返回类型)。也就是说,它们通常比你想要的多一点,所以对于这种情况,它们可能有点笨拙(如果有用的话)。
答案 2 :(得分:3)
我不认为这是const-correctness本身的缺陷,而是缺乏方便的能力来推广一个方法而不是cv-qualifiers(同样我们可以通过模板推广类型)。假设你能写出类似的东西:
template<cvqual CV>
inline CV ITEMIDLIST* GetNextItem(CV ITEMIDLIST * pidl)
{
return pidl ? reinterpret_cast<CV ITEMIDLIST *>(reinterpret_cast<CV BYTE *>(pidl) + pidl->mkid.cb) : NULL;
}
ITEMIDLIST o;
const ITEMIDLIST co;
ITEMIDLIST* po = GetNextItem(&o); // CV is deduced to be nothing
ITEMIDLIST* pco = GetNextItem(&co); // CV is deduced to be "const"
现在你可以用模板元编程实际做这种事情,但这会得到 凌乱的快速:
template<class T, class TProto>
struct make_same_cv_as {
typedef T result;
};
template<class T, class TProto>
struct make_same_cv_as<T, const TProto> {
typedef const T result;
};
template<class T, class TProto>
struct make_same_cv_as<T, volatile TProto> {
typedef volatile T result;
};
template<class T, class TProto>
struct make_same_cv_as<T, const volatile TProto> {
typedef const volatile T result;
};
template<class CV_ITEMIDLIST>
inline CV_ITEMIDLIST* GetNextItem(CV_ITEMIDLIST* pidl)
{
return pidl ? reinterpret_cast<CV_ITEMIDLIST*>(reinterpret_cast<typename make_same_cv_as<BYTE, CV_ITEMIDLIST>::result*>(pidl) + pidl->mkid.cb) : NULL;
}
上述问题是所有模板的常见问题 - 它允许您传递任何随机类型的对象,只要它具有正确名称的成员,而不仅仅是ITEMIDLIST
。当然,您可以使用各种“静态断言”实现,但这本身也是一种破解。
或者,您可以使用模板化版本重用.cpp文件中的代码,然后将其包装到const / non-const对中并在标头中公开它。这样,你几乎只复制功能签名。
答案 3 :(得分:2)
你的函数正在指向pidl,它是const或非const。您的功能将是修改参数,或者不会 - 选择一个并完成它。如果该函数还修改了您的对象,则使该函数为非const。我不明白你为什么需要重复的功能。
答案 4 :(得分:2)
你现在有一些解决方法......
关于最佳实践:提供const和非const版本。这是最容易维护和使用(IMO)。在最低级别提供它们,以便它可以最容易地传播。不要让客户端投射,你会抛出实现细节,问题和缺点。他们应该能够在没有黑客的情况下使用你的课程。
我真的不知道一个理想的解决方案......我认为关键字最终会最简单(我拒绝使用宏)。如果我需要const和非const版本(这是非常频繁的),我只需要定义它两次(就像你一样),并且记住要始终将它们彼此相邻。
答案 5 :(得分:1)
我认为很难绕过,如果你在STL中看到vector
这样的东西,你就会有同样的事情:
iterator begin() {
return (iterator(_Myfirst, this));
}
const_iterator begin() const {
return (iterator(_Myfirst, this));
}
/A.B。
答案 6 :(得分:1)
在我的工作中,我开发了类似于Pavel Minaev proposed的解决方案。但是我使用它有点不同,我认为它使事情变得更简单。
首先,您需要两个元函数:标识和const添加。如果您使用Boost,可以从boost::mpl::identity
获取两者(Boost.MPL来自boost::add_const
而Boost.TypeTraits来自Boost)。然而,它们(特别是在这种有限的情况下)是如此微不足道,以至于可以在不参考{{3}}的情况下定义它们。
编辑: C ++ 0x提供add_const
(在type_traits
标头中)元函数,因此此解决方案刚刚成为有点简单。 Visual C ++ 2010也提供identity
(在utility
标题中)。
定义如下
template<typename T>
struct identity
{
typedef T type;
};
和
template<typename T>
struct add_const
{
typedef const T type;
};
现在通常,您将提供成员函数的单个实现作为private
(或protected
,如果需要某种方式)static
函数,将this
作为一个参数(如果省略非成员函数this
)。
static
函数还有一个模板参数,它是处理constness的元函数。实际函数将调用此函数指定为identity
(非const
版本)或add_const
(const
版本)的模板参数。
通常情况如下:
class MyClass
{
public:
Type1* fun(
Type2& arg)
{
return fun_impl<identity>(this, arg);
}
const Type1* fun(
const Type2& arg) const
{
return fun_impl<add_const>(this, arg);
}
private:
template<template<typename Type> class Constness>
static typename Constness<Type1>::type* fun_impl(
typename Constness<MyClass>::type* p_this,
typename Constness<Type2>::type& arg)
{
// Do the implementation using Constness each time constness
// of the type differs.
}
};
请注意,此技巧不会强制您在头文件中实现。由于fun_impl
为private
,因此无论如何都不应在MyClass
之外使用它。因此,您可以将其定义移动到源文件(让类中的声明可以访问类内部)并将fun
定义移动到源文件中。
这只是有点冗长,但是如果有更长的非平凡功能,它会得到回报。
我认为这很自然。毕竟你刚才说你必须为两种不同的类型重复相同的算法(函数实现)(const
一个和非 - const
一个)。这就是模板的用途。用于编写满足某些基本概念的任何类型的算法。
答案 7 :(得分:0)
我认为如果你需要抛弃一个变量的const来使用它,那么你的“消费者”代码就不是正确的。您是否可以提供一两个测试案例?
答案 8 :(得分:0)
您的案例中不需要两个版本。非const事物将隐式转换为const事物,但反之亦然。从您的函数名称来看,GetNextItem
似乎没有理由修改pidl
,因此您可以像这样重写它:
inline ITEMIDLIST * GetNextItem(const ITEMIDLIST * pidl);
然后客户端可以使用const或非const ITEMIDLIST
来调用它,它只会起作用:
ITEMIDLIST* item1;
const ITEMIDLIST* item2;
item1 = GetNextItem(item1);
item2 = GetNextItem(item2);
答案 9 :(得分:0)
从您的示例中,这听起来像是具有传递函数的特殊情况,您希望返回类型与参数的类型完全匹配。一种可能性是使用模板。例如:
template<typename T> // T should be a (possibly const) ITEMIDLIST *
inline T GetNextItem(T pidl)
{
return pidl
? reinterpret_cast<T>(reinterpret_cast<const BYTE *>(pidl) + pidl->mkid.cb)
: NULL;
}
答案 10 :(得分:0)
您可以使用模板。
template<typename T, typename U>
inline T* GetNextItem(T* pidl)
{
return pidl ? reinterpret_cast<T*>(reinterpret_cast<U*>(pidl) + pidl->mkid.cb) : NULL;
}
并像
一样使用它们ITEMDLIST* foo = GetNextItem<ITEMDLIST, BYTE>(bar);
const ITEMDLIST* constfoo = GetNextItem<const ITEMDLIST, const BYTE>(constbar);
如果你厌倦了输入,请使用某些typedef。
如果你的函数没有使用具有相同更改常量的第二种类型,编译器将自动推断出要使用的函数,你可以省略模板参数。
但我认为ITEMDLIST的结构中可能存在更深层次的问题。是否可以从ITEMDLIST派生?差点忘了我的win32次......糟糕的回忆......
编辑:当然,您可以滥用预处理器。多数民众赞成的东西。因为你已经在win32上,你可以完全转向黑暗面,不再重要; - )