0

Lets say I have the following:

struct point
{
  double x;
  double y;
  double z;
};

I can write the following:

void point_mult(point& p, double c) { p.x *= c; p.y *= c; p.z *= c; }
void point_add(point& p, const point& p2) { p.x += p2.x; p.y += p2.y; p.z += p2.z; }

So I can then do the following:

point p{1,2,3};
point_mult(p, 2);
point_add(p, point{4,5,6});

This requires no copies of point, and only two constructions, namely the construction of point{1,2,3} and the construction of point{4,5,6}. I believe this applies even if point_add, point_mult and point{...} are in separate compilation units (i.e. can't be inlined).

However, I'd like to write code in a more functional style like this:

point p = point_add(point_mult(point{1,2,3}, 2), point{4,5,6});

How can I write point_mult and point_add such that no copies are required (even if point_mult and point_add are in separate compilation units), or is functional style forced to be not as efficient in C++?

Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
Clinton
  • 22,361
  • 15
  • 67
  • 163
  • 5
    Why do you assume that "copying" means "not as efficient"? I don't see anything there that couldn't be elided. And since it's all on the stack and in small structs, it's likely done in place. – Nicol Bolas Feb 29 '12 at 06:47
  • 1
    I think you will always run into the fundamental problem of supporting non-commutative operations. The order of evaluation of the comma-separated arguments of a function is undetermined. – juanchopanza Feb 29 '12 at 06:49
  • 5
    Above and beyond @Nicol's statement that in many cases a decent compiler will be able to elide copies in this case, I also would like to recommend that you don't worry about this too much until profiling has shown that extra copies are an actual performance bottleneck in your application. – Sven Feb 29 '12 at 07:16
  • 1
    -1 for asking about a performance problem before verifying that it exists. – Potatoswatter Feb 29 '12 at 09:54

5 Answers5

4

Let's ignore the implicit fallacy of the question (namely that copying automatically means reduced efficiency). And let's also ignore the question of whether any copying would actually happen, or whether it would all be elided away by any half-decent compiler. Let's just take it on face value: can this be done without copying?

Yes, and it is probably the only other legitimate use for r-value references (though the previously ignored stipulations make this use case dubious):

point &&point_mult(point &&p, double c);

Of course, this will only bind to temporaries. So you would need an alternate version for l-values:

point &point_mult(point &p, double c);

The point is that you pass the references through as they are, either as references to temporaries or references to l-values.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • But wouldn't the lvalue version still need to modify the argument to avoid a copy? In my mind, this is not good functional programming style. And if you accept that a function shouldn't modify its arguments, it follows that the return value *must* be a copy. The only exception is when the arguments are temporaries that will be discarded, in which case you can reuse their storage, which the rvalue reference (and in most cases, copy elision by the compiler) provides for. If the argument is not a temporary I don't think you can avoid a copy and still have good functional style in any language. – Sven Feb 29 '12 at 16:15
  • @Sven: Re-read my first paragraph. He wanted a version that didn't copy, that specifically modifies its arguments. That's what he asked for, so that's what I gave him. – Nicol Bolas Feb 29 '12 at 17:25
3

It can be done with really ugly template metaprogramming. For example eigen uses templates so that expressions like matrix1 + matrix2 * matrix3 don't need to create any temporaries. The gist of how it works is the + and * operators for matrices don't return Matrix objects but instead return some kind of matrix expression object which is templatized on the types of the expression paramaters. This matrix expression object can then compute parts of the expression only when they are needed instead of creating temporary objects to store the result of subexpressions.

Actually implementing this can get quite messy. Have a look at Eigen's source if your interested. Boost's uBlas also does something similar, though it's not as extensively as eigen.

David Brown
  • 13,336
  • 4
  • 38
  • 55
2

An efficient (and generalized) technique is expression templates. You can read a nice introductory explanation here.

It's difficult to implement and being based on templates, you cannot use separate compilation units, but it's very efficient. An interesting application in symbolic computation is parsing: Boost.Spirit builds very efficient parsers out of them.

C++11 auto keywords helps usage on practical programming tasks, as always when dealing with complex types, see this other answer.

Community
  • 1
  • 1
CapelliC
  • 59,646
  • 5
  • 47
  • 90
  • 2
    Doesn't `auto` break expression templates? If they take their arguments by reference (which is a requirement of the OP. No "copying"), then the reference will be dangling, since it's referring to temporaries. – Nicol Bolas Feb 29 '12 at 07:36
  • @Nicol Bolas:I think that *auto* can hide important syntax details. But given the complexity of C++ templates, it's almost indispensable. A basic understanding of dangling references is anyway required to squeeze efficiency from C++. But I must confess I'm currently brushing up my C++, and I not yet touched C++11 (still on gcc 4.5). Don't you think expression templates solve OP's problem? – CapelliC Feb 29 '12 at 08:11
  • The problem with **auto** is that it allows to create named instances of complicated expression template types that were supposed to be implicitly converted to the result type. The user is not even supposed to be aware of the complex types involved. As you can create a named instance, you can postpone that conversion until some later time. The problem is, by then, references to temporaries will have gone bad. - I also doubt that it will help in this particular case where there is minimal benefit from avoiding copies in the first place. – visitor Feb 29 '12 at 10:05
  • @visitor: that seems almost a bug, I'll try ASAP. I expect that *auto* at most inhibits further templated expressions, not that breaks evaluation. – CapelliC Feb 29 '12 at 10:54
2

First, why not use "better" functions ?

struct Point {
  double x;
  double y;
  double z;

  Point& operator+=(Point const& right) {
    x += right.x; y += right.y; z += right.z;
    return *this;
  }

  Point& operator*=(double f) {
    x *= f; y *= f; z *= f;
    return *this;
  }
};

Now it can be used as:

Point p = ((Point{1,2,3} *= 2) += Point{4,5,6});

But I truly think that you worry too much about copies here (and performance).

  1. Make it work
  2. Make it fast

If you don't have anything that already works, talking about performance is akin to chasing mills... bottlenecks are rarely where we thought they would be.

Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
  • Isn't this bad as `const auto& p = ...` will result in a dangling reference? I've been told not to return rvalue references to parameters, isn't there the same issue with lvalue references? – Clinton Feb 29 '12 at 07:54
  • @Clinton: there is no dangling reference here. There would be if you were to try and keep a reference `Point & p` but this is not the case here. All references are valid during the computation (since temporaries live until the end of the full expression evaluation) and none is kept afterward, so no issue. Also note that the signature of `+=` and `*=` are canonical. – Matthieu M. Feb 29 '12 at 08:09
-1

Change the definition of point_mult() to:

point& point_mult(point& p, double c) { p.x *= c; p.y *= c; p.z *= c; return p; }
^^^^^^                                                                ^^^^^^^^^

And call it as:

point & p = point_add(point_mult(*new point{1,2,3}, 2), point{4,5,6});
     ^^^                         ^^^^^

there is no copy involved. However, you have to later do delete &p; for releasing the memory.

iammilind
  • 68,093
  • 33
  • 169
  • 336
  • 1
    This syntax does not allow him to pass in temporaries, as in his second example, as temporaries will not bind to non-const references. Also, a function having the side-effect of modifying its arguments is not exactly good functional style. – Sven Feb 29 '12 at 06:55
  • 3
    I feel delete &p should be avoided. Perhaps a shared pointer solution is better? – Johan Lundberg Feb 29 '12 at 07:05
  • 1
    Dynamic memory allocation brings way more overhead than just making a copy? In order to avoid a trivial operation, you'd suggest replacing it with a complicated operation? - Also, is it just me, but not storing the result of **new** in a non-pointer is just hideous. Before now I was always sure that if I see a reference I don't try to delete that address. Now I can never know... – visitor Feb 29 '12 at 10:12