I've looked for an answer to this one, but I can't seem to find anything, so I'm asking here:
Do reference parameters decay into pointers where it is logically necessary?
Let me explain what I mean:
If I declare a function with a reference to an int as a parameter:
void sum(int& a, const int& b) { a = b; }
(assuming that this won't be inlined)
The logical assumption would be that calling this function can be optimized by not passing any parameters, but by letting the function access the variables that are already on the stack. Changing these directly prevents the need for passing pointers.
Problem with this is that (again, assuming this doesn't get inlined), if the function is called from a ton of different places, the relevant values for each call are potentially in different places in the stack, which means the call can't be optimized.
Does that mean that, in those cases (which could potentially make up the majority of one's cases if the function is called from a ton of different places in the code), the reference decays into a pointer, which gets passed to the function and used to influence the variables in the outer scope?
Bonus question: If this is true, does that mean I should consider caching referenced parameters inside of function bodies, so that I avoid the hidden dereferences that come with passing these references? I would then conservatively access the actual reference parameters, only when I need to actually write something to them. Is this approach warranted or is it best to trust the compiler to cache the values for me if it deems the cost of dereferencing higher than the cost of copying them one time?
Code for bonus question:
void sum(int& a, const int& b) {
int aCached = a;
// Do processing that required reading from a with aCached.
// Do processing the requires writing to a with the a reference.
a = b;
}
Bonus bonus question: Is it safe to assume (assuming everything above is true), that, when "const int& b" is passed, the compiler will be smart enough to pass b by value when passing by pointer isn't efficient enough? My reasoning behind this is that values are ok for "const int& b" because you never try to write to it, only read.
CodePudding user response:
The compiler can decide to implement references as pointers, or inlining or any other method it chooses to use. In terms of performance, it's irrelevant. The compiler can and will do whatever it wants to when it comes to optimization. The compiler can implement your reference as a pass-by-value if it wants to (and if it's valid to do so in the specific situation). Caching the result won't help because the compiler will do that anyways. If you want to explicitly tell the compiler that the value might change (because of another thread that has access to the same pointer), you need to use the keyword volatile (or std::atomic if you're not already using a std::mutex). If you don't use the keyword volatile, the compiler will almost certainly cache the result for you (if appropriate). There are, however, at least 2 actual differences in the rules between pointers and references.
- Taking the address (pointer) of a temporary value (rvalue) is undefined behavior in C .
- References are immutable, sometimes need to be wrapped in std::ref.
Here I'll provide examples for both differences.
This code using references is valid:
static int do_stuff(const int& i)
{
}
int main()
{
do_stuff(5);
return 0;
}
But this code has undefined behavior (in practice it will probably still work):
static int do_stuff(const int* i)
{
}
int main()
{
do_stuff(&5);
return 0;
}
That's because taking the address of a temporary value (non lvalue) is undefined behavior in C . The value is not guaranteed to have an address. Note that taking the address like this is valid:
static int do_stuff(const int& i)
{
int *ptr = &i;
}
int main()
{
do_stuff(5);
return 0;
}
Because inside of the function do_stuff, the variable has a name and is therefore an lvalue. That means that by the time it's inside of do_stuff it's guaranteed to have an address.
So that's one difference between a pointer an a reference in C . There is another difference, and that is the constness / immutability.
On important thing to know about in C is the use for the helper function std::ref. Consider the following code:
#include <functional>
#include <thread>
#include <future>
#include <chrono>
#include <iostream>
struct important_t
{
int val = 0;
};
static void work(const volatile important_t& arg)
{
std::cout << "Doing work..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(3));
}
int main()
{
important_t my_object;
{
std::cout << "Starting thread" << std::endl;
std::future<void> t = std::async(std::launch::async, work, std::ref(my_object));
std::cout << "Waiting for thread to finish" << std::endl;
}
return 0;
}
The above code will compile just fine, and is perfectly valid C code. But if you wrote it like this:
std::future<void> t = std::async(std::launch::async, work, my_object);
It wouldn't compile. That's because of the std::ref. The reason that the code doesn't compile without std::ref is that the function std::async (and also std::thread) requires each and every one of the objects being passed as function parameters to be copy constructible. That demonstrates a fundamental difference between references and all other built-in types in C . References are immutable, and there's no way to make them editable. Consider the following code:
#include <iostream>
int main()
{
// Perfectly valid
// Prints 5
{
int val = 0;
int& val_ref = val;
val_ref = 5;
std::cout << val << std::endl;
}
// Compiler error:
// A reference must always be initialized.
// A reference will always point to the same value throughout its lifetime.
{
int val = 0;
int& val_ref;
val_ref = val;
val_ref = 5;
std::cout << val << std::endl;
}
// We will encounter a similar compiler error with a const pointer:
// A const value must always be initialized.
// A const pointer will always point to the same value throughout its lifetime.
{
int val = 0;
int *const val_ptr;
val_ref = &val;
val_ref = 5;
std::cout << val << std::endl;
}
return 0;
}
That leads to the conclusion that a reference is not the same thing as a pointer in C . It's almost the same thing as a const pointer. Just a little bit of clarification:
A const pointer to a const int:
void do_stuff(const int *const val)
{
int i;
val = 5; // Error
val = &i; // Error
}
A const pointer to an int:
void do_stuff(int *const val)
{
int i;
val = 5; // Allowed. The int is not const.
val = &i; // Error
}
A pointer to a const int:
void do_stuff(const int* val)
{
int i;
val = 5; // Error
val = &i; // Allowed
}
An int reference in C is the closest thing to a const pointer to an int. The int is editable, the pointer is not.