Passing literal as a const ref parameter
Asked Answered
V

3

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.

Vastah answered 11/8, 2017 at 23:58 Comment(2)
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?Bullace
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.Messenger
T
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.

Typeset answered 12/8, 2017 at 1:29 Comment(5)
"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?Vastah
"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?Vastah
@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.Typeset
fair enough. I was hoping there is a way to avoid that, but I will probably just ask it as a separate questionVastah
In case anyone is interested, here is P0386R2 that covers inline variables in c++17.Vastah
M
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.

Messenger answered 12/8, 2017 at 0:41 Comment(15)
"The use of move semantics actually dangerous" - that isn't move semantics. I think this question is more complicated than you realize.Tippler
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.Tippler
Missing scope in bar::baz was a typo. I've fixed itVastah
@Tippler 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.Messenger
@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?Messenger
@Swift it won't link due to missing definition of bar::baz. I am using a modern compiler. Please see this on coileru.Vastah
@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 placeTippler
@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 :)Messenger
@Swift it compiles because of optimizations. I will make it more clear in my questionVastah
@Tippler 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 thatMessenger
@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 compilerMessenger
@Tippler it's shame that __PRETTY_FUNCTION__ is GNU only, other compiler analogs do not show the supposed prototype of the function.Messenger
@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.Tippler
@Tippler yeah, though while standard-complying compilerswill not provide such mysteries like MS or Lahey do.Hell, Microsoft sometimes plainly breaks SFINAEMessenger
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.Typeset
B
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; }
Botnick answered 12/8, 2017 at 1:7 Comment(1)
Missing scope in bar::baz was a typo. I've fixed it.Vastah

© 2022 - 2024 — McMap. All rights reserved.