18

Imagine the following simplified code:

#include <iostream>
void foo(const int& x) { do_something_with(x); }

int main() { foo(42); return 0; }

(1) Optimizations aside, what happens when 42 is passed to foo?

Does the compiler stick 42 somewhere (on the stack?) and pass its address to foo?

(1a) Is there anything in the standard that dictates what is to be done in this situation (or is it strictly up to the compiler)?


Now, imagine slightly different code:

#include <iostream>
void foo(const int& x) { do_something_with(x); }

struct bar { static constexpr int baz = 42; };

int main() { foo(bar::baz); return 0; }

It won't link, unless I define int bar::baz; (due to ODR?).

(2) Besides ODR, why can't the compiler do whatever it did with 42 above?


An obvious way to simplify things is to define foo as:

void foo(int x) { do_something_with(x); }

However, what would one do in case of a template? Eg:

template<typename T>
void foo(T&& x) { do_something_with(std::forward<T>(x)); }

(3) Is there an elegant way to tell foo to accept x by value for primitive types? Or do I need to specialize it with SFINAE or some such?

EDIT: Modified what happens inside foo as it's irrelevant to this question.

  • Maybe code generated for T&& and T is same for 42 and it simply is a value in a register in the function regardless of how it is passed to function? – huseyin tugrul buyukisik Aug 12 '17 at 00:32
  • if this question about implementation of compiler, that 's really not defined, as a constexpr value, compiler may `movl` that 42 into register just within the code. – Swift - Friday Pie Aug 12 '17 at 01:13

3 Answers3

13

Does the compiler stick 42 somewhere (on the stack?) and pass its address to foo?

A temporary object of type const int is created, initialized with the prvalue expression 42, and bound to the reference.

In practice, if foo is not inlined, that requires allocating space on the stack, storing 42 into it, and passing the address.

Is there anything in the standard that dictates what is to be done in this situation (or is it strictly up to the compiler)?

[dcl.init.ref].

Besides ODR, why can't the compiler do whatever it did with 42 above?

Because according to the language, the reference is bound to the object bar::baz, and unless the compiler knows exactly what foo is doing at the point where it is compiling the call, then it has to assume that this is significant. For example, if foo contains an assert(&x == &bar::baz);, that must not fire with foo(bar::baz).

(In C++17, baz is implicitly inline as a constexpr static data member; no separate definition is required.)

Is there an elegant way to tell foo to accept x by value for primitive types?

There is generally not much point in doing this in the absence of profiling data showing that pass-by-reference is actually causing problems, but if you really need to do it for some reason, adding (possibly SFINAE-constrained) overloads would be the way to go.

T.C.
  • 133,968
  • 17
  • 288
  • 421
  • "In C++17, baz is implicitly inline as a constexpr static data member..." Could you please add a reference to the standard? What if type of `bar` is eg `std::chrono::milliseconds`? Will it still be inlined? – Super-intelligent Shade Aug 12 '17 at 15:02
  • "There is generally not much point in doing this in the absence of profiling data showing that pass-by-reference is actually causing problem..." I was thinking along the lines of a class that's very expensive to copy. If I understand you correctly, one must use template overloads in this case? Even if `foo` is a lengthy function? – Super-intelligent Shade Aug 12 '17 at 16:58
  • @InnocentBystander If your template can accept expensive-to-copy classes, pass by reference. Then, if some call with primitive types is actually causing performance problems, add overloads to pass those cheap-to-copy types by value. – T.C. Aug 12 '17 at 17:15
  • fair enough. I was hoping there is a way to avoid that, but I will probably just ask it as a separate question – Super-intelligent Shade Aug 12 '17 at 18:13
  • In case anyone is interested, here is [P0386R2](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0386r2.pdf) that covers inline variables in c++17. – Super-intelligent Shade Aug 13 '17 at 15:19
3

With C++17 that code compiles perfectly considering usage of bar::baz as inline, with C++14 the template requires prvalue as an argument, so compiler retains a symbol for bar::baz in object code. Which will not get resolved because you didn't had that declaration. constexpr should be treated as constprvalue or rvalues by compiler, in code generation that may lead to different approach. E.g. if called function is inline, compiler may generate code that is using that particular value as constant argument of processor's instruction. Keywords here are "should be" and "may", which are as different from "must" as usual disclaimer clause in general standard documentation states.

For a primitive type, for a temporal value and constexpr there will be no difference, in which template signature you do use. How actually compiler implements it, depends on platform and compiler... and calling conventions used. we can't really even tell if something is on stack for sure, because some platform do NOT have stack or it is implemented differently from stack on x86 platform. Multiple modern calling conventions do use registers of CPU to pass arguments.

If your compiler is modern enough you don't need references at all, copy elision would save you from extra copy operations. To prove that:

#include <iostream>

template<typename T>
void foo(T x) { std::cout << x.baz << std::endl; }


#include <iostream>
using namespace std;

struct bar
{
    int baz;

    bar(const int b = 0): baz(b)
    {
        cout << "Constructor called" << endl;
    }    

    bar(const bar &b): baz(b.baz)  //copy constructor
    {
        cout << "Copy constructor called" << endl;
    } 
};

int main() 
{ 
    foo(bar(42)); 
}

will result in output:

Constructor called
42

Passing by reference, by a const reference wouldn't cost more than passing by value, especially for templates. If you need different semantics, you would require explicit specialization of template. Some older compilers couldn't support the latter in proper way.

template<typename T>
void foo(const T& x) { std::cout << x.baz << std::endl; }

// ...

bar b(42);
foo(b); 

Output:

Constructor called
42

Non-const reference would not allow us to forward argument, if it was an lvalue, e.g

template<typename T>
void foo(T& x) { std::cout << x.baz << std::endl; }
// ...
foo(bar(42)); 

by calling this template (called perfect forwarding )

template<typename T>
void foo(T&& x) { std::cout << x << std::endl; }

one would be able to avoid forwarding problems, though this process would also involve copy elision. Compiler deduces template parameter as follows from C++17

template <class T> int f(T&& heisenreference);
template <class T> int g(const T&&);
int i;
int n1 = f(i); // calls f<int&>(int&)
int n2 = f(0); // calls f<int>(int&&)
int n3 = g(i); // error: would call g<int>(const int&&), which
               // would bind an rvalue reference to an lvalue

A forwarding reference is an rvalue reference to a cv-unqualified template parameter. If P is a forwarding reference and the argument is an lvalue, the type “lvalue reference to A” is used in place of A for type deduction.

Swift - Friday Pie
  • 12,777
  • 2
  • 19
  • 42
  • 1
    *"The use of move semantics actually dangerous"* - [that isn't move semantics](https://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers). I think this question is more complicated than you realize. – WhozCraig Aug 12 '17 at 00:43
  • You missed *my* point. That isn't necessarily an rvalue-reference, and in the case of the OPs code, it most-definitely is not. [See example](http://ideone.com/cK4PUt). – WhozCraig Aug 12 '17 at 01:03
  • 1
    Missing scope in `bar::baz` was a typo. I've fixed it – Super-intelligent Shade Aug 12 '17 at 01:12
  • @WhozCraig it IS rvalue reference, term universal reference isn't present in standard. what compiler does with code is not defined by standard and is moot point, only result does matter. – Swift - Friday Pie Aug 12 '17 at 01:12
  • @InnocentBystander ah, ok, then it's unclear why you couldn't link it. are you using some old gcc or clang that is pre-1x standard? – Swift - Friday Pie Aug 12 '17 at 01:19
  • @Swift it won't link due to missing definition of `bar::baz`. I am using a modern compiler. Please see [this](http://coliru.stacked-crooked.com/a/d7bcde904b4cfd75) on coileru. – Super-intelligent Shade Aug 12 '17 at 01:29
  • 1
    @Swift Well, then you'l hopefully notice the *result* you speak up deduces neither of those arguments to rvalue references after collapsing, which *is* defined per the standard. Nor does it change what I initially said. There are no move semantics, the subject of which has since-been excised from your answer. And before the accusation is volleyed, no, I was *not* the person(s) that down-ticked this answer. My only real issue was resolved with your edit, and it was minor enough that I wouldn't have down-ticked in the first place – WhozCraig Aug 12 '17 at 01:33
  • @Innocent Bystander Ah, I see.. latest GCC compiles that, and by standard that should work, I can write that down only to bug or an oversight. Checked different versions, seem up to 7.x they do not link it. I used to use Intel compiler that supported that feature way earlier, using GCC when I test stuff from home :) – Swift - Friday Pie Aug 12 '17 at 01:35
  • 1
    @Swift it compiles because of optimizations. I will make it more clear in my question – Super-intelligent Shade Aug 12 '17 at 01:37
  • @WhozCraig I wasn't caring to the downtick. Atcually I think that was due to typo in code, caused by wrong paste. That output let me understand what did you meant. the rvalue ref can be degraded to a const reference or an rvalue, degradation is of type is deduced properly there, I won't argue with that – Swift - Friday Pie Aug 12 '17 at 01:40
  • @Innocent Bystander it compiles because C++17 says so. In C++14 it doesn't, but some 7.x GCC that must support C++17 features still won't compile it unless optimization is ticked. coileru.uses way older compiler – Swift - Friday Pie Aug 12 '17 at 01:50
  • @WhozCraig it's shame that `__PRETTY_FUNCTION__` is GNU only, other compiler analogs do not show the supposed prototype of the function. – Swift - Friday Pie Aug 12 '17 at 02:44
  • 1
    @Swift clang supports it as well, but I concur. It truly rocks, and is just plain awesome for throwing together deduction tests when you have doubts wtf is going on when template programming. It has saved me many frustrating hours of deduction-mystery. – WhozCraig Aug 12 '17 at 02:59
  • @WhozCraig yeah, though while standard-complying compilerswill not provide such mysteries like MS or Lahey do.Hell, Microsoft sometimes plainly breaks SFINAE – Swift - Friday Pie Aug 12 '17 at 06:10
  • If you take a forwarding reference and then unconditionally move from it, your code is almost certainly broken. The idea that I can't continue to use a URNG I passed to `std::shuffle` (which takes it by forwarding reference) is absurd on its face. The point of taking a `const &` is to avoid copying in the lvalue case, which you completely failed to address. – T.C. Aug 12 '17 at 08:27
2

Your example #1. Constant location completely depends on the compiler and is not defined in a standard. GCC on Linux might allocate such constants in a static read-only memory section. Optimization will probably remove it all together.

Your example #2 will not compile (before link). Due to the scoping rules. So you need bar::baz there.

example #3, i usually do this:

template<typename T>
    void foo(const T& x) { std::cout << x << std::endl; }
Michał Fita
  • 1,183
  • 1
  • 7
  • 24
Serge
  • 11,616
  • 3
  • 18
  • 28