Home > Net >  Can I avoid a second object when using a unary operator on a temporary?
Can I avoid a second object when using a unary operator on a temporary?

Time:10-08

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.

  1. The object is generally expensive to copy (to justify the use of move),
  2. The operation can be applied in-place without necessarily making a copy (otherwise we can just return the necessary copy always).
  3. 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.

  • Related