The usual meaning of unary operators such as bitwise inversion, postfix increment, and unary minus is to return a modified copy of their argument. When their argument is a temporary, is there a way to modify that original object, thus avoiding the creation and destruction of a second object?
The examples below both involve two objects:
#include <utility>
#include <iostream>
using namespace std;
struct X {
~X() { cout << "destroy\n"; }
X() { cout << "default construct\n"; }
X(const X& ) { cout << "copy construct\n"; }
X( X&&) { cout << "move construct\n"; }
X& operator=(const X& ) { cout << "copy assign\n"; return *this; }
X& operator=( X&&) { cout << "move assign\n"; return *this; }
X operator~() const & { cout << "~lvalue\n"; return *this; }
X operator~() && { cout << "~rvalue\n"; return move(*this); }
};
int main(int, char**) {
{
cout << "Example 1\n";
auto a = X();
auto b = ~a;
}
{
cout << "Example 2\n";
auto a = ~X();
}
}
I get this output (ignore that ~lvalue and copy construct "appear" out of order):
Example 1
default construct
~lvalue
copy construct
destroy
destroy
Example 2
default construct
~rvalue
move construct
destroy
destroy
Is there a way to rewrite the struct so that example 2 only creates and destroys a single object?
CodePudding user response:
Is there a way to rewrite the struct so that example 2 only creates and destroys a single object?
But your code says to create two objects. You create a prvalue and then perform an operation on it which is not initializing some other object. That operation requires manifesting a temporary from that prvalue. That's object 1.
Then you use auto a
to create a new object. That's object 2.
It doesn't matter what operator~
is doing; you must manifest a temporary from the prvalue, since operator~
needs to have a this
. Even if the return value is a &&
to this
(which is absolutely should not, because operator~
should be a const
function), that wouldn't help, because a
is a separate object. You can move construct a
, but it won't ever not be a separate object.
If you only want one object, you have to use code that says to create only one object: auto a = X(); a.invert();
.
CodePudding user response:
Your question has some assumptions to justify the complication.
- The object is generally expensive to copy (to justify the use of move),
- The operation can be applied in-place without necessarily making a copy (otherwise we can just return the necessary copy always).
- The in-place operation is at least somewhat cheaper than the corresponding out-of-place operation. (the "somewhat" is related to the cost of copying)
The C 98 way
In this case, to first order, as @NicolBolas said, it is better to just give the user access to this in-place operation because it probably it is very useful even beyond your scenario.
In this case the class is fairly simple,
struct X {
void tilde_inplace() { ... inplace operation ... }
};
and nothing else in regards of this hypothetical operation (which I didn't call ~
because it doesn't return a copy, as you indicated.)
usage:
auto x = X{};
x.tilde_inplace();
Very simple, no copies to worry about, no moves etc. You can make a copy if you need to.
auto const x = X{};
auto x_copy = x;
x_copy.tilde_inplace();
This is very C 98 and it is fine.
Syntactic sugar/Functional interface
If one entertains the idea that for some reason one needs a "functional" interface to the operation one can do the following. But remember that this is basically syntactic sugar... unless you really really need it for "generic programming" reasons:
struct X {
void tilde_inplace() { ... inplace operation ... }
X tilde() const { auto tmp = *this; tmp.tilde_inplace(); return tmp;}
};
This is an illustration of the assumption that at the end there is no other optimal way to do this other than in-place (otherwise tilde()
can have an independent implementation).
Robust (always work) solution
Extrapolating to exploit temporaries (rvalues):
struct X {
void tilde_inplace() { ... inplace operation ... }
X tilde() const& { auto tmp = *this ; tmp.tilde_inplace(); return tmp; /*IF there is no better way*/}
X tilde() && { auto tmp = std::move(*this); tmp.tilde_inplace(); return tmp;}
};
This assumes very little about X
, I think it is very robust.
One might be tempted to just remove tmp
in the second case:
X tilde() && { this->tilde_inplace(); return *this;}
but, does it work? in the sense that it does elide the copy of *this
with RVO). in all versions of C 11/14/17/20? I don't know.
Probably not.
To be sure the signature has to change to this more strange code:
X&& tilde() && { this->tilde_inplace(); return std::move(*this);}
Although this is the only case in which I managed to make the functional interface as lean as the C 98 interface (https://godbolt.org/z/EdP7KzzqM),
I would say doesn't correspond to the canonical signature of a conventional unary operator such as operator~
, because it returns a reference.
Another advantage of the "robust/uniform" solution is that it can be written as a single free or friend function.
struct X {
void tilde_inplace() { ... inplace operation ... }
template<class Self>
auto free_tilde(Self self) {self.tilde_inplace(); return self;}
}; // to be further simplified in C 23 with "deduced-this".
See, no move in the implementation, free_tilde
can be even noexcept
if tilde_inplace
is noexcept
.
Summary
So, in summary, for a robust solution, replace "tilde
" by "operator~
" above and you have the following recipe for your case.
Optionally hide the in-place interface (but there is nothing wrong with it unless there is a commitment to make classes immutable and to functional programming).
struct X {
private:
void tilde_inplace() { ... inplace operation ... }
public:
X operator~() const& { ... possibly out-of-place implementation if better at all, or copy inplace }
X operator~() && { auto tmp = std::move(*this); tmp.tilde_inplace(); return tmp;}
};
Feel free to destroy this answer if you don't agree. (I am willing to change my opinion.) Also you can experiment here: https://godbolt.org/z/EdP7KzzqM
NOTE: Are there cases where the unary out-of-place operation is faster than an in-place (if both exists)? I couldn't find many examples. The fact that in-place operation has an initial advantage edge is the reason we tend to use mutation our programs. One case I found is that of FFT, which maybe marginal. Using the library FFTW I found that out-of-place FFT is a bit faster (-10%) than in-place FFT but both are faster than allocation plus out-of-place ( 10%).
timing(FFT(in, out)) < timing(FFT(io, io)) < timing(out-allocation FFT(in, out))
(for example, 4 seconds, 5 seconds, 6 seconds on some of the timing with GB size data). I don't know how much this is an artifact of the library implementation. Unfortunately to exploit all these cases one has to mess with the constructor itself in my experiments.