经常性的成本难题

时间:2009-11-30 21:13:13

标签: c++

我经常发现自己必须定义一个函数的两个版本,以便有一个是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版本
  • 或只是一个版本,然后要求该代码的消费者按照他们的方式进行演员表?

或者是否有更好的解决方法? 是否正在对语言本身进行工作以完全缓解或消除这个问题?

奖励积分:

  • 你觉得这是C ++ const-system的不幸副产品
  • 或者你觉得这等于触及奥林匹斯山的高度吗?

编辑:

如果我只提供第一个 - 接受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是中立的。它实际上并没有修改它给定的东西,但它的输出需要传播其参数的常量。

注意:第二次编辑格式化。

11 个答案:

答案 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_constBoost.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_constconst版本)的模板参数。

通常情况如下:

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_implprivate,因此无论如何都不应在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上,你可以完全转向黑暗面,不再重要; - )

相关问题