This happens because when overloaded operators are defined as member functions, they follow some semantics which are more related to calling a member function, not to the behavior of the built-in operator. Note that by default, if we declare a non-static member function like:
class X {
public:
void f();
X g();
};
then we can call it on both lvalue and rvalue class type expressions:
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
When overload resolution for an operator expression selects a member function, the expression is changed to be just a call to that member function, so it follows the same rules:
++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.
This sort of non-equivalence between built-in operators and overloaded operators also happens with the left subexpression of assignment: the pointless statement std::string() = std::string();
is legal, but the statement int() = int();
is not legal.
But you noted in a comment "I want to design a class that prevents ++a++
". There are at least two ways to do that.
First, you could use a non-member operator instead of a member. Most overloaded operators can be implemented as either a member or non-member, where the class type needs to be added as an additional first parameter type of the non-member function. For example, if a
has class type, the expression ++a
will attempt to find a function as if it were a.operator++()
and also a function as if it were operator++(a)
; and the expression a++
will look for functions for the expressions a.operator++(0)
or operator++(a, 0)
.
(This pattern of trying both ways does not apply to functions named operator=
, operator()
, operator[]
, or operator->
, because they may only be defined as non-static member functions, never as non-members. Functions named operator new
, operator new[]
, operator delete
, or operator delete[]
, plus user-defined literal functions whose names start like operator ""
, follow entirely different sets of rules.)
And when the class argument matches a real function parameter, instead of the "implicit object parameter" of a non-static member function, the type of reference used in the parameter, if any, controls as usual whether an argument can be an lvalue, rvalue, or either.
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
}
Another way is to add ref-qualifiers to member functions, which were added to the language in the C++11 version as a specific way of controlling whether a member function's implicit object argument must be an lvalue or rvalue:
class C {
public:
C& operator++() & {
// Some internal increment logic.
return *this;
}
C operator++(int) & {
C temp(*this);
++temp;
return temp;
}
};
Notice the &
between the parameter list and the start of the body. This restricts the function to only accept an lvalue of type C
(or something that implicitly converts to a C&
reference) as the implicit object argument, similarly to how a const
in the same spot allows the implicit object argument to have type const C
. If you wanted a function to require an lvalue but allow that lvalue to optionally be const
, the const
comes before the ref-qualifier: 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++().
}
To get operator=
to act more like it does for a scalar type, we can't use a non-member function, because the language only allows member operator=
declarations, but the ref-qualifier will similarly work. You're even allowed to use the = default;
syntax to have the compiler generate the body, even though the function isn't declared in exactly the same way an implicitly-declared assignment function would have been.
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
}
++i++
is one of those statements you really don't want to see in your code. – StiversB{}.f()
to compile, so would++B{}
. – Lingerie