15

Is there a functional difference between:

void foo(const Bar& bar) {
  Bar bar_copy(bar);
  // Do stuff with bar_copy
}

and

void foo(Bar bar) {
  // Do stuff with bar
}
David Rodríguez - dribeas
  • 204,818
  • 23
  • 294
  • 489
wrhall
  • 1,288
  • 1
  • 12
  • 26
  • 4
    I assume you mean `Bar bar_copy(bar);`? – danielschemmel Apr 02 '15 at 21:00
  • oops, yeah. I wrote it correctly in my term and didn't copy it over. Sorry. – wrhall Apr 02 '15 at 21:02
  • 3
    I've been using this to declare functions taking incomplete types (by reference), definitions of which are later included in `.cpp` file. – LogicStuff Apr 02 '15 at 21:02
  • 1
    @LogicStuff you use the top one? Why can't you do that with the bottom one? – wrhall Apr 02 '15 at 21:04
  • 1
    @wrhall Because parameter in function declaration must be **complete** type if it's not passed by reference or pointer. – LogicStuff Apr 02 '15 at 21:13
  • 3
    @wrhall - the compiler needs to know how to construct (and of course allocate) `Bar`, as it is put on the stack before the function call. The size of the object needs to be known by the caller. So it has to be completely specified. References and pointers are only passed as a pointer (typically) which has a known size (i.e. `sizeof(void*)`) so you don't need to know what size object you have until you are inside the function. – Mark Lakata Apr 02 '15 at 21:15
  • 1
    @LogicStuff: Wrong. It only needs to be complete at the point of use and at the point of definition, The declaration needs only know a name. – Xeo Apr 02 '15 at 23:58

3 Answers3

22

Yes, there is a valuable difference.

void foo(Bar bar) can copy-construct or move-construct bar, depending on the calling context.

And when a temporary is passed to foo(Bar bar), your compiler may be able to construct that temporary directly where bar is expected to be. Hat tip to template boy.

Your function void foo(const Bar& bar) always performs a copy.

Your function void foo(Bar bar) may perform a copy or a move or possibly neither.

Drew Dormann
  • 59,987
  • 13
  • 123
  • 180
  • 3
    You also miss out on an optimization opportunity as when the initializer to the parameter is a temporary the copy or move can be elided entirely. – template boy Apr 02 '15 at 22:12
14

Yes, there are differences. While the most obvious one is that the type of the function changes (and thus the type of its function pointer), there are also some less obvious implications:

Move-constructible but not copy-constructible Bar

For example, assume the following call to foo:

foo(Bar());

For the first version, this will be passed by a reference to const bar and then copied using the copy constructor. For the second version, the compiler will attempt the move-constructor first.

This means, that the only the second version can be called by types that are move-constructible only, like std::unique_ptr. In fact, the manually forced copy will not even allow compilation of the function.

Obviously, this can be mitigated by adding a slight complication:

void foo(Bar&& bar) {
    // Do something with bar.
    // As it is an rvalue-reference, you need not copy it.
}

void foo(Bar const& bar) {
    Bar bar_copy(bar);
    foo(std::move(bar_copy));
}

Access specifiers

Interestingly, there is another difference: The context in which access permissions are checked.

Consider the following Bar:

class Bar
{
    Bar(Bar const&) = default;
    Bar(Bar&&) = default;

public:
    Bar() = default;

    friend int main();
};

Now, the reference-and-copy version will error out, while the parameter-as-value version will not complain:

void fooA(const Bar& bar)
{
    //Bar bar_copy(bar); // error: 'constexpr Bar::Bar(const Bar&)' is private
}

void fooB(Bar bar) { } // OK

Since we have declared main to be a friend, the following call is allowed (note that the friend would not be necessary if, e.g. the actual call was made in a static member function of Bar):

int main()
{
    fooB(Bar()); // OK: Main is friend
}

Completeness of Bar at the call site

As has been observed in the comments, if you wish Bar to be an incomplete type at the call site, it is possible to use the pass-by-reference version, as this does not require the call site to be able to allocate an object of type Bar.

Copy Elision Side-Effects

C++11 12.8/31:

When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the copy/move constructor and/or destructor for the object have side effects. In such cases, the implementation treats the source and target of the omitted copy/move operation as simply two different ways of referring to the same object [...]

  • [...]
  • when a temporary class object that has not been bound to a reference (12.2) would be copied/moved to a class object with the same cv-unqualified type, the copy/move operation can be omitted by constructing the temporary object directly into the target of the omitted copy/move
  • [...]

Obviously, only the call-by-value version meets this criterium - after passing by reference, the parameter is bound to a reference after all. Beyond an observable difference, this also means that an optimization oppurtunity is lost.

danielschemmel
  • 10,885
  • 1
  • 36
  • 58
4

There are some differences.

void foo(const Bar& bar) {
  Bar bar_copy(bar);
  // Do stuff with bar_copy
}

doesn't allow to avoid the copy even if bar was a temporary and avoid also the move of bar.

Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • Does that make it strictly worse if you can just pass by value [ie there is a comment on the question that says they use pass-by-reference and then copy for functions taking incomplete types, which they presumably can't use in the pass-by-value version]? – wrhall Apr 02 '15 at 21:06
  • 1
    @wrhall: If you pass by value, you allow move construction of `bar` and from `bar`. – Jarod42 Apr 02 '15 at 21:11
  • Right -- so in the case where you can pass by value, you probably should, right? Or are there reasons not to? – wrhall Apr 02 '15 at 21:14
  • 1
    As state in the comment, for *forward declaration*. For "simplicity", as the preferred way is to pass by const reference in other case which is more frequent. – Jarod42 Apr 02 '15 at 21:20