1

Assume you are providing a client library with a function that has multiple reference arguments.

For the sake of simplicity lets assume we have 3 arguments and those are various references to int (so you can assume, that these constants would also be created by initializer_list).

From a call site it should be possible to pass ad hoc created constants (rvalue) to functions (think of testing code in this case) as well as real references to objects being owned by another entity.

So far I've come up with the following solutions:

void bar(int &x, int &y, int &z) { }

// use a template wrapper to turn rvalues into lvalues.
template <typename T, typename U, typename V>
void foo(T &&t, U &&u, V &&v) { 
    bar(t,u,v);
}

// create a humongous amount of overloads
void baz(int &&x, int &y, int &z) { }
void baz(int &x, int &&y, int &z) { }
void baz(int &&x, int &&y, int &z) { }
void baz(int &x, int &y, int &&z) { }
void baz(int &&x, int &y, int &&z) { }
void baz(int &x, int &&y, int &&z) { }
void baz(int &&x, int &&y, int &&z) { }

// claim ownership of the objects and create temporaries
void bam(int x, int y, int z) { }

int main() {

    int i = 1;
    foo(i,2,3);
    foo(2,i,3);
    foo(2,3,i);

    bar(i,2,3); // doesn't compile
    bar(2,i,3);
    bar(2,3,i);

    baz(i,2,3); // requires 8 overloads
    baz(2,i,3); 
    baz(2,3,i);

    return 0;
}   

I'm not completely satisfied with all of the solutions as every one of them has drawbacks. Is there a cleaner alternative to this problem?

Alexander Oh
  • 24,223
  • 14
  • 73
  • 76
  • 4
    I would just default to C++03 `const&` unless the profiler shows some big advantage for adding an rref overload for a given use case. – Billy ONeal Jun 03 '15 at 23:49
  • 1
    This is the perfect forwarding problem. The canonical solution is to use rvalue references, directly(universal reference and srtd::forward) or indirectly(pass by value). What are the drawbacks you are talking about? Of course, using `int` to illustrate is a bad idea. (NB : Your template wrapper solution is incorrect since you are missing std::forward.) – Pradhan Jun 04 '15 at 00:10
  • @Pradhan I specifically didn't want to use a `map>` in the example. the drawbacks are in the comments. so far I have a standard variadic template function that I paste in this case that slowly keeps littering my source code. copying large stuff is not the way I want to go and creating a lot of overloads as well. – Alexander Oh Jun 04 '15 at 00:17
  • @Alex Could you add comments for the pass-by-value and the "universal reference" template solutions as well? This information would help answer your specific question. – Pradhan Jun 04 '15 at 00:20
  • @Pradhan they are already in the function definition? – Alexander Oh Jun 04 '15 at 00:21
  • @Pradhan and hte perfect forwarding example doesn't compile, because the wrapper turns rvalue references into lvalues to pass. – Alexander Oh Jun 04 '15 at 00:24
  • "`bar(i,2,3); // doesn't compile`" how does that not compile? Please post an actual problem, with actual attempts to fix it, and point out what actually went wrong. Simplify it as much as you can, but make sure there are actual errors there. I suspect your error is the fact that `bar` takes `const int&` and not `int&` or somesuch? An example of details on what you are doing to the referenced data inside may also be useful: you are giving non-working half-solutions to a half-problem: a cursory description of a real problem can let us avoid your half-problem entirely. – Yakk - Adam Nevraumont Jun 04 '15 at 00:47
  • 1
    @Alex you didn't mention the most obvious solution `void qux(int const &x, int const &y, int const &z) { }`. So I guess you have some idea that you might want the parameters to be mutable, but that doesn't square with `bar(i,2,3)` because `2` is not mutable. So you need to nail down a description of what the input conditions and behaviour for this function really will be. – M.M Jun 04 '15 at 10:25

3 Answers3

2

This question is actually not trivial but there some guidelines that have evolved over the years and have actually not changed too much with C++11. For the following we will assume that you have a side-effect free, free-standing function (guidelines are slightly different for constructors and certain member functions).

What I would recommend for a free-standing function is:

  1. Pass primitive data types (int, double, bool, etc.) by value
  2. Pass complex data types by const reference (e.g. const std::vector&) unless you need a copy within the function then pass by value
  3. If your function is templated use const reference (e.g. const T&) since both lvalue and rvalue will bind to const references
  4. If your function is templated and you intend to perfect forward then use forwarding references (e.g. T&&, also called universal references)

So for your example using integers I would use the bam function:

void bam(int x, int y, int z) { }

There is an interesting talk by Herb Sutter given at the CppCon last year that among other stuff covers input arguments to functions: https://www.youtube.com/watch?v=xnqTKD8uD64

Another interesting post on stackoverflow related to this question: Correct usage of rvalue references as parameters

Update for constructors:

The main difference for constructors to the recommendations above is a stronger emphasis on copying input and making the object owned by the class. The same holds for setters. The main reason is safer resource management (to avoid data races or accessing invalid memory).

Community
  • 1
  • 1
Christian Blume
  • 188
  • 1
  • 9
1

This is the perfect forwarding problem. The way you avoid the 8 overloads of baz is to use the so-called universal references:

template <typename T, typename U, typename V>
void baz(T &&t, U &&u, V &&v) { }

In the above, t, u and v faithfully replicate the r/l-valueness for the parameters. For example, say your original intended function was this:

void baz(vector<int> &t, vector<int> &u, vector<int> &v) { }

and the other 7 overloads. With the templated version, if you call baz(a,b,c) where a and b are rvalues, while c isn't, T and U will be deduced to be vector<int>&& while V will be deduced to be vector<int>&. Thus, you have all the information you need in this implementation.

Pradhan
  • 16,391
  • 3
  • 44
  • 59
  • @Alex Sorry, could not figure out what was missing from the comments. Could you comment on this answer and help me understand your question? – Pradhan Jun 04 '15 at 00:32
1
template<class T>
struct lrvalue {
  T&t;
  lrvalue(T&in):t(in){}
  lrvalue(T&&in):t(in){}
};

take lrvalue<int>. Problem solved? You can dress them up, maybe inherit from reference_wrapper or somesuch instead of T&t.

You can also cast when calling:

template<class T>
T& lvalue(T&&t){return t;}
template<class T>
T& lvalue(T&t)=delete;//optional

now if you want to pass foo{} to foo& you can. lvalue(foo{}).

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524