创建一个C++整数类,其行为与整型整数类型完全相同

8

我几天前遇到了一个小问题,关于IT技术方面的内容。我向我的朋友在面试中询问了这个问题。

最初的面试问题是:“以下代码的输出结果是什么?”

int i = 2;
i = i++ + i++;

正确答案是 ((2 + 2) + 1) + 1 = 6,也就是说,在加法操作后执行两次后缀递增,然后再赋值。

接下来,我想创建一个简单的类,包含一个整数,并重载 operator+() 和 operator++(int),以便在日志中查看运算符执行的确切顺序。

以下是代码:

class A
{
public:
A(int _data) : data(_data) { }

A &operator=(const A& _rhs)
{
    data = _rhs.data;
    cout<<" -- assign: "<<data<<endl;
}

A operator++(int _unused)
{
    A _tmp = data;
    data++;

    cout<<" -- post-increment: "<<data<<endl;
    return _tmp;
}

A operator+(const A &_rhs)
{
    A _tmp = data + _rhs.data;

    cout<<" -- addition: "<<data<<"+"<<_rhs.data<<endl;
    return _tmp;
}

inline operator int() const { return data; }

private:
    int data;
};

结果非常令人沮丧:
-- post-increment: 3
-- post-increment: 4
-- addition: 3+2
-- assign: 5

对于较简单的构造,比如(A _dt2 = a++;),它的作用与预期一致,但运算符的执行顺序并不像整型那样。

我猜这可能是编译器特定的问题:

$ gcc --version
gcc (Ubuntu 4.4.3-4ubuntu5) 4.4.3
Copyright (C) 2009 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

所以,我有点迷惑 :)

6
我很确定第一个片段的输出未定义。 - Alexandre C.
2
是的,第一个代码片段存在未定义行为。 - sharptooth
2
可能是未定义行为和序列点的重复问题。 - Alok Save
10
隐藏的面试问题是:“你能检测到这是未定义行为吗?” - Alok Save
@Als:感谢您提供更好的链接放在我的答案中,但它并不是直接的重复。问题的主要话题是关于整数类的仿真和增量方面的。 - Xeo
显示剩余2条评论
4个回答

16

最初的面试问题是:“以下代码的输出是什么?”

int i = 2;
i = i++ + i++;

正确答案是 未定义行为,因为你在同一个变量上多次进行修改而没有序列点。

C++03标准§5 [expr] p4:

除非另有说明,否则对单个运算符的操作数和单个表达式的子表达式的求值顺序以及副作用发生的顺序是未指定的。

这可能不能回答你的实际问题,但即使你创建了一个类似于整数的类并重载了operator++(int)operator+(A const&),函数参数的求值顺序也是未指定的,编译器可以按任意顺序进行求值,因此结果是未指定的。


如果两个运算符都被重载,那么行为就不再是未定义的了 - 每个运算符变成了一个函数调用,这引入了一个序列点。然而结果仍然是未指定的,正如你所说的。 - Mike Seymour
@Mike:不要只是删除你的评论然后重新发一个。:P 但是没错,运算符变成函数调用的乐趣确实是存在的。 - Xeo
你可能会想要引用标准中的一句话,C++03第5节:表达式,第4段: 除非有特别说明[例如对于&&和||的特殊规则],否则对于单独运算符的操作数和子表达式以及单独表达式的副作用执行顺序是未指定 - Alok Save
@MikeSeymour,@Xeo:在重载的情况下,“+”操作数的唯一“求值顺序”是未指定的;但这不是UB,如果实现正确地处理了+和++的幺半群属性(将++视为返回先前值的+e),则i = i ++ + i ++的最终结果可能是明确定义的。 - user396672
抱歉,需要“+”的可交换性,这几乎足够了 :) - user396672

7
除了其他人已经指出的内容之外:仅看你问题的标题——“创建 C++ 整数类以完全等同于整数类型”,我应该指出一个完全不同的原因,为什么这是不可能的

据我所知,使用类无法模拟||&&运算符的快捷行为,即无论如何都会对操作数的两侧进行评估。

编辑:请查看评论。“据我所知”似乎还不够。然而,Steve Jessop有一个不同的例子,证明了总体上的观点是正确的。

这与您的增量问题完全无关,但与您的问题标题相关,因此我认为应该提到。


3
如果你想模拟整型类型,我认为你不需要重新实现那些操作符。操作符 &&|| 可以自动调用你的类的 operator bool(),这样它们的短路行为就会被保留。 - Fabio A.
虽然如此,不可能的原因更强烈,因为将类A的实例隐式转换为(例如)long会“使用掉”您允许在隐式转换中的1个用户定义转换,而将int转换为long则不会。因此,如果您有另一个带有以long为参数的构造函数的类B,并且int i = 0; A a = i;,您会发现B b = i;是允许的,但B b = a;不被允许,因为转换链A-> int -> long -> B涉及两个用户定义转换。 - Steve Jessop
@Fabio A.:这就是你从(遥远和模糊的)记忆中做出陈述的结果。我肯定在某个地方读过这个,而且有98%的把握它是在Scott Meyer的优秀著作“Effective C++”中的某个地方,但我无法准确回忆起来,并且无法在测试代码中再现该效果。更令人沮丧的是,我不再拥有那本书,现在也无法查阅。/我很不开心...但史蒂夫在这里提出了另一个强有力的观点,因此总体信息仍然成立。 - DevSolar
顺便提一下,这里有一个SafeInt类,它将任何整数类型作为模板参数复制,并添加了一些检查以防止溢出和除以0:http://bit.ly/uWX4Lh - Fabio A.
@Fabio:当然可以,但问题在于是否可能使用类A来模拟int的行为。如果B不是与A相同的类,则A的行为与int的行为不同。因此,在这方面,A并没有模仿int,因为世界上有很多具有long构造函数的类。 - Steve Jessop
显示剩余2条评论

4
正确答案是 ((2 + 2) + 1) + 1 = 6,即在加法操作之后,在赋值之前执行了两次后缀递增。但这不是正确的答案:
除非另有说明,否则单个运算符的操作数和单个表达式的子表达式的求值顺序以及副作用发生的顺序均未指定。在前一个和下一个序列点之间,标量对象的存储值最多被一个表达式的求值修改一次。此外,先前的值仅用于确定要存储的值。对于完整表达式的每个允许的子表达式排序,都必须满足本段的要求;否则,行为未定义。- ISO-IEC-14882

1

实际上,在早期你犯了一个相当严重的错误。

最初的面试问题是:“以下代码的输出结果是什么?”

int i = 2;
i = i++ + i++;

这个问题的正确答案是“输出未定义”。

通过在没有中间序列点的情况下修改和读取变量,您正在调用未定义的行为。


更具体地说,在这种情况下,让你感到困扰的是参数传递给+运算符的顺序是未定义的;而且在一般情况下,对于运算符和函数都是如此,只有少数几个值得注意的例外,即短路逻辑运算符。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接