正确模拟[]运算符

时间:2012-04-17 06:36:43

标签: c++ operators

考虑包含数字数组的Number类的情况。我希望类用户可以轻松访问数字,所以我重载了[]运算符,这样用户就可以用10的幂来选择一个数字,例如。 number [2]是一个代表数百的数字,依此类推。该类是索引安全的 - 如果用户指定索引超出范围,则返回0(为10 = 0010)。

但是,设置一个数字需要更多的努力,因为如果指数超过持有数字的内部表的大小,后者必须被扩展。

很明显,必须选择不同的方法,用户是否想要检索数字以及何时想要存储数字。我提出了以下解决方案(这些类是部分的,只是为了表明这个想法):

class Number;

class Digit
{
    friend class Number;

private:
    Number number;
    int exponent;

    Digit(Number & newNumber, int newExponent) 
        : number(newNumber), exponent(newExponent)
    {
    }

public:
    operator unsigned char() 
    {
        return number.GetDigit(exponent);
    }

    void operator = (unsigned char digit) 
    {
        number.SetDigit(exponent, digit);
    }
};

class Number
{
    friend class Digit;

private:
    unsigned char GetDigit(int exponent) { ... };
    void SetDigit(int exponent, unsigned char value) { ... };

public:
    Digit operator [] (int exponent)
    {
        return Digit(*this, exponent);
    }
}

Digit类中的运算符显然会调用一些私有Number的方法,这样用户就可以分配一个值并检索一个值。我也可以采取不同的行动,无论用户是想要检索价值还是想要存储它。

然而,const存在问题。如果Number类实例是const,则不能调用operator [],除非它也被标记为const。但是,Digit类无法实例化,因为它需要Number&,而const Number&通过了。

一个解决方案涉及使用const_cast,但感觉就像使用hack - 更不用说,删除const将允许用户使用Digit的operator =修改数字实例。有没有办法以不同于使用const_cast或编写另一个类来解决这个问题,比如ConstDigit?


另一个问题:我不希望用户存储Digit实例,因为它只是为了用户的舒适而提供。私有拷贝构造函数阻止用户存储数字,如:

auto digit = number[5];

但是,他仍然可以通过参考存储数字:

auto digit & = number[5];

数字通过引用传递给数字(参见数字构造函数)以防止指针出现问题,但在这种情况下它不会阻止AV:

auto number = new Number();
auto & digit = (*number)[5];
delete number;
digit = 12;

我可能禁止用户在Number上使用new运算符,但是可能还有另一种方法吗?


答案较长,为什么我不能通过引用返回数字。

数字保存为两个单独的向量,一个用于整数部分,一个用于小数部分(我简化了相关示例,但这并不重要)。例如,

123.456

存储为

intPart : {3, 2, 1}
fracPart : {4, 5, 6}

如果用户请求第100位的数字,我只是检查它是否在数组范围之外并返回0(因为 - 正如我所提到的,10 = 0010 = 00010 = ......等等)

但是,如果用户希望设置数字,我必须扩展数组以填充所有剩余的指数,例如。

100 // user wants to set millions' digit to 5
5 [000] 100 // i have to add these empty digits

当然我也可以在[]运算符中执行此操作。但是如果用户决定将第100位设置为0,我将不得不添加98位数字,如果他试图这样做,我甚至无法检查(并且该数字应该以尽可能小的表示存储领先或尾随零存在)。在任命之后我无法压缩存储空间。

当然,其中一个解决方案是删除[]运算符并使用getter / setter。但重点是使类易于使用,[]运算符比一对getter / setter方法更能实现这个假设:)

1 个答案:

答案 0 :(得分:1)

我将从你的第二个问题开始回答:如果你参考,你必须确保它在你想要访问它时仍然有效。在您的示例中,绝对不是这种情况。我认为这应该是一个简化的例子,以显示在通过先前存储的引用访问之前被引用的对象被某处销毁的情况。你无法解决这个问题(直接)。

你也可以在STL中偶然发现这个“问题”。例如,如果你引用了一个向量元素,然后在向量中插入一些东西,请注意引用(可能)变得无效,因为容器内容可能由于新的内存分配而移动到其他地方。

我认为避免这种情况的唯一方法是使用智能指针。至少要保证对对象的访问是安全的。这将无法让您访问对象,如果某些代码更改了背面,请用数字替换(*number)[5]

现在回答您的第一个问题:目前,我无法看到您打算如何在Number对象上设置某个数字。你有一个数字赋值运算符,但由于Number::operator[]按值返回数字,你不能用它来设置数字。

假设我的(错误)理解仅仅是由于代码示例不完整。请适当延长。现在,我假设Digit的{​​{1}}已在Number的构造函数中初始化,并且Number仅用于访问。

通常,您只需创建一个返回operator[]的所述运算符的const版本。如果您不是100%确定对象不是const,请不要使用const Digit删除const_cast。否则,您将获得未定义的行为。但是,使用const向非const对象添加/删除const_cast是安全的。这对于重用const和非const访问方法的代码非常有用。非const方法可以添加const,调用const访问方法,然后从返回值中安全删除const(这取决于您的实现)。有关更多详细信息,请参阅Scott Meyers的“Effective C ++”。

那就是说,我必须补充一点,我通常希望在调用const时引用一个对象,这可能允许我更改对象(例如:operator[]std::vector) 。如果它仅用于只读访问,则通过const引用或const值返回,而不是通过(非const)值返回。否则,错误就像

std::map

会发生并且不被注意。读者会假设Number[5] = 12; 已分配给数字12。实际上,它只存储在所述stored in Number[5]副本中。如果Digit仅返回Number::operator[],编译器将捕获此信息。

修改

我看到你如何返回按值,但仍然修改了原始数字。但是,我仍然觉得这个界面不直观。在大多数案例中,如果提供一致的,甚至更好的直观行为,那么为您的类提供运算符是明智的。查看您的const Digit声明,我只看到它按值返回operator[]。你的界面告诉我,我得到一份副本。没有办法看到它在内部仍然修改我的Digit对象。

因此,如果您使用getter / setter方法,我认为界面会更清晰。