Super-intelligent Shade
Super-intelligent Shade

Reputation: 6449

Passing literal as a const ref parameter

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.

Upvotes: 18

Views: 2640

Answers (3)

Serge
Serge

Reputation: 12354

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; }

Upvotes: 2

T.C.
T.C.

Reputation: 137315

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.

Upvotes: 13

Swift - Friday Pie
Swift - Friday Pie

Reputation: 14589

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.

Upvotes: 3

Related Questions