C++后增量:对象与基元类型

C++ post-increment: objects vs primitive types

本文关键字:类型 对象 C++      更新时间:2023-10-16

我们不能对右值使用预递增:

int i = 0;
int j = ++i++; // Compile error: lvalue required

如果我们定义一个类:

class A
{
public:
A & operator++()
{
return *this;
}
A operator++(int)
{
A temp(*this);
return temp;
}
};

然后我们可以编译:

A i;
A j = ++i++;

对象和 int 数据类型有什么区别

j = ++i++;

用 A 编译但不用 int 编译?

发生这种情况是因为当重载运算符定义为成员函数时,它们遵循一些与调用成员函数更相关的语义,而不是与内置运算符的行为更相关。 请注意,默认情况下,如果我们声明一个非静态成员函数,例如:

class X {
public:
void f();
X g();
};

然后我们可以在左值和右值类类型表达式上调用它:

X().f();   // okay, the X object is prvalue
X x;
x.f();     // okay, the X object is lvalue
x.g().f(); // also okay, x.g() is prvalue

当运算符表达式的重载解析选择成员函数时,表达式将更改为仅调用该成员函数,因此它遵循相同的规则:

++A(); // okay, transformed to A().operator++(), called on prvalue
A a;
++a;   // okay, transformed to a.operator++(), called on lvalue
++a++; // also technically okay, transformed to a.operator++(0).operator++(),
// a.operator++(0) is a prvalue.

内置运算符和重载运算符之间的这种不等价性也发生在赋值的左子表达式中:无意义的语句std::string() = std::string();是合法的,但语句int() = int();是不合法的。

但是您在评论中指出"我想设计一个防止++a++的类"。 至少有两种方法可以做到这一点。

首先,可以使用非成员运算符而不是成员。 大多数重载运算符可以作为成员或非成员实现,其中类类型需要添加为非成员函数的附加第一个参数类型。 例如,如果a具有类类型,则表达式++a将尝试查找一个好像a.operator++()的函数和一个好像operator++(a)的函数;表达式a++将查找表达式a.operator++(0)operator++(a, 0)的函数。

(这种尝试两种方式的模式不适用于名为operator=operator()operator[]operator->的函数,因为它们只能定义为非静态成员函数,而不能定义为非成员。 名为operator newoperator new[]operator deleteoperator delete[]的函数,加上名称开头像operator ""的用户定义的文字函数,遵循完全不同的规则集。

当类参数与实函数参数匹配时,而不是非静态成员函数的"隐式对象参数",参数中使用的引用类型(如果有)像往常一样控制参数是否可以是左值、右值或两者之一。

class B {
public:
// Both increment operators are valid only on lvalues.
friend B& operator++(B& b) {
// Some internal increment logic.
return b;
}
friend B operator++(B& b, int) {
B temp(b);
++temp;
return temp;
}
};
void test_B() {
++B(); // Error: Tried operator++(B()), can't pass
// rvalue B() to B& parameter
B b;
++b;   // Okay: Transformed to operator++(b), b is lvalue
++b++; // Error: Tried operator++(operator++(b,0)), but
// operator++(b,0) is prvalue and can't pass to B& parameter
}

另一种方法是将 ref 限定符添加到成员函数中,这些限定符在 C++11 版本中被添加到语言中,作为控制成员函数的隐式对象参数必须是左值还是右值的特定方法:

class C {
public:
C& operator++() & {
// Some internal increment logic.
return *this;
}
C operator++(int) & {
C temp(*this);
++temp;
return temp;
}
};

请注意参数列表和正文开头之间的&。 这将函数限制为仅接受类型C(或隐式转换为C&引用的内容)的左值作为隐式对象参数,类似于同一位置的const如何允许隐式对象参数具有类型const C。 如果您希望函数需要左值,但允许选择性地const该左值,则const位于 ref 限定符之前:void f() const &;

void test_C() {
++C(); // Error: Tried C().operator++(), doesn't allow rvalue C()
// as implicit object parameter
C c;
++c;   // Okay: Transformed to c.operator++(), c is lvalue
++c++; // Error: Tried c.operator++(0).operator++(), but
// c.operator++(0) is prvalue, not allowed as implicit object
// parameter of operator++().
}

为了让operator=的行为更像标量类型,我们不能使用非成员函数,因为该语言只允许成员operator=声明,但 ref 限定符将同样工作。 您甚至可以使用= default;语法让编译器生成主体,即使该函数的声明方式与隐式声明的赋值函数的方式并不完全相同。

class D {
public:
D() = default;
D(const D&) = default;
D(D&&) = default;
D& operator=(const D&) & = default;
D& operator=(D&&) & = default;
};
void test_D() {
D() = D(); // Error: implicit object argument (left-hand side) must
// be an lvalue
}

它... 只是。有一些约束仅适用于基元类型,而不适用于类类型(嗯,你已经找到了最明显的一个!

这主要是因为内置类型的运算符是一回事,而对于类来说,它们只是伪装的成员函数,因此是完全不同的野兽。

这是否令人困惑?我不知道;或。

真的有令人信服的理由吗?我不知道;可能不是。基元类型有一定的惯性:为什么要仅仅因为引入类而更改 C 语言中的内容?允许这样做有什么好处?另一方面,禁止它用于类是不是过于严格,operator++的实现可以做一些你作为语言设计师没有想到的事情?