C++ post-增量:对象与原始类型
C++ post-increment: objects vs primitive types
我们不能对右值使用预增量:
int i = 0;
int j = ++i++; // Compile error: lvalue required
如果我们定义一个class:
class A
{
public:
A & operator++()
{
return *this;
}
A operator++(int)
{
A temp(*this);
return temp;
}
};
然后我们就可以编译了:
A i;
A j = ++i++;
A对象和int数据类型有什么区别
j = ++i++;
使用 A 编译但不使用 int?
它……就是这样。有一些约束只适用于基本类型而不适用于 class 类型(好吧,你已经找到了最明显的一个!)。
这主要是因为内置类型的运算符是一回事,而对于 classes 它们只是伪装的成员函数,因此是完全不同的野兽。
这令人困惑吗?我不知道;也许。
是否有真正令人信服的理由?我不知道;可能不会。原始类型有一定的惰性:为什么仅仅因为引入 classes 就改变 C 中的某些东西?允许这样做有什么好处?另一方面,对于 classes 禁止它会不会过于严格,因为 operator++
的实现可以做一些作为语言设计者没有想到的事情?
发生这种情况是因为当重载运算符被定义为成员函数时,它们遵循一些与调用成员函数更相关的语义,而不是内置运算符的行为。请注意,默认情况下,如果我们声明一个非静态成员函数,如:
class X {
public:
void f();
X g();
};
然后我们可以在左值和右值 class 类型表达式上调用它:
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();
是不合法的.
但是您在评论中指出 "I want to design a class that prevents ++a++
"。至少有两种方法可以做到这一点。
首先,您可以使用非成员运算符代替成员。大多数重载运算符可以实现为成员或非成员,其中 class 类型需要添加为非成员函数的附加第一个参数类型。例如,如果 a
具有 class 类型,则表达式 ++a
将尝试查找一个函数,就好像它是 a.operator++()
一样,也会尝试找到一个函数,就好像它是 operator++(a)
;并且表达式 a++
将查找表达式 a.operator++(0)
或 operator++(a, 0)
.
的函数
(这种尝试两种方式的模式不适用于名为 operator=
、operator()
、operator[]
或 operator->
的函数,因为它们只能定义为非静态成员函数,从不作为非成员。名为 operator new
、operator new[]
、operator delete
或 operator delete[]
的函数,以及名称以 operator ""
,遵循完全不同的规则集。)
并且当 class 参数匹配一个真正的函数参数,而不是非静态成员函数的 "implicit object parameter" 时,参数中使用的引用类型(如果有的话)控制为通常参数可以是左值、右值或两者之一。
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
}
另一种方法是向成员函数添加引用限定符,这是在 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
位于引用限定符之前: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 -qualifier 将类似地工作。您甚至可以使用 = 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
}
我们不能对右值使用预增量:
int i = 0;
int j = ++i++; // Compile error: lvalue required
如果我们定义一个class:
class A
{
public:
A & operator++()
{
return *this;
}
A operator++(int)
{
A temp(*this);
return temp;
}
};
然后我们就可以编译了:
A i;
A j = ++i++;
A对象和int数据类型有什么区别
j = ++i++;
使用 A 编译但不使用 int?
它……就是这样。有一些约束只适用于基本类型而不适用于 class 类型(好吧,你已经找到了最明显的一个!)。
这主要是因为内置类型的运算符是一回事,而对于 classes 它们只是伪装的成员函数,因此是完全不同的野兽。
这令人困惑吗?我不知道;也许。
是否有真正令人信服的理由?我不知道;可能不会。原始类型有一定的惰性:为什么仅仅因为引入 classes 就改变 C 中的某些东西?允许这样做有什么好处?另一方面,对于 classes 禁止它会不会过于严格,因为 operator++
的实现可以做一些作为语言设计者没有想到的事情?
发生这种情况是因为当重载运算符被定义为成员函数时,它们遵循一些与调用成员函数更相关的语义,而不是内置运算符的行为。请注意,默认情况下,如果我们声明一个非静态成员函数,如:
class X {
public:
void f();
X g();
};
然后我们可以在左值和右值 class 类型表达式上调用它:
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();
是不合法的.
但是您在评论中指出 "I want to design a class that prevents ++a++
"。至少有两种方法可以做到这一点。
首先,您可以使用非成员运算符代替成员。大多数重载运算符可以实现为成员或非成员,其中 class 类型需要添加为非成员函数的附加第一个参数类型。例如,如果 a
具有 class 类型,则表达式 ++a
将尝试查找一个函数,就好像它是 a.operator++()
一样,也会尝试找到一个函数,就好像它是 operator++(a)
;并且表达式 a++
将查找表达式 a.operator++(0)
或 operator++(a, 0)
.
(这种尝试两种方式的模式不适用于名为 operator=
、operator()
、operator[]
或 operator->
的函数,因为它们只能定义为非静态成员函数,从不作为非成员。名为 operator new
、operator new[]
、operator delete
或 operator delete[]
的函数,以及名称以 operator ""
,遵循完全不同的规则集。)
并且当 class 参数匹配一个真正的函数参数,而不是非静态成员函数的 "implicit object parameter" 时,参数中使用的引用类型(如果有的话)控制为通常参数可以是左值、右值或两者之一。
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
}
另一种方法是向成员函数添加引用限定符,这是在 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
位于引用限定符之前: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 -qualifier 将类似地工作。您甚至可以使用 = 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
}