Pass by value and pass by reference are the two ways C++ hands a function its inputs. Passing by value gives the function a private copy it can change freely. Passing by reference gives it an alias to the original, so changes are visible to the caller. Choosing correctly is the difference between a function that surprises its caller and one that behaves exactly as its name suggests.
Pass by value: the function gets a copy
A by-value parameter is initialised from the argument. The function works on its own copy, and the caller's variable is untouched.
#include
void apply_discount(double price) {
price = price * 0.9;
std::cout << "inside function: " << price << '\n';
}
int main() {
double loaf_price = 12.00;
apply_discount(loaf_price);
std::cout << "after function: " << loaf_price << '\n';
return 0;
}The output shows the two values clearly:
inside function: 10.8
after function: 12The function's change to price disappeared when the function returned. This is the safe default: the caller keeps its data, and the function cannot surprise anyone.
Pass by reference: the function gets an alias
A reference parameter is another name for the caller's object. Writing through it changes the original.
#include
void apply_discount(double& price) {
price = price * 0.9;
std::cout << "inside function: " << price << '\n';
}
int main() {
double loaf_price = 12.00;
apply_discount(loaf_price);
std::cout << "after function: " << loaf_price << '\n';
return 0;
}Now the output is different:
inside function: 10.8
after function: 10.8The & in the parameter list is the whole difference. Note that the call site did not change: apply_discount(loaf_price) looks identical in both programs. That is a real ergonomic cost of pass by reference — the caller cannot tell from the call alone whether its variable will be modified.
Const references give the best of both
A const reference combines the efficiency of a reference with the safety of a copy. The function sees the caller's object without copying it, and the const makes modifying it a compile error.
#include
#include
int count_characters(const std::string& text) {
int count = 0;
for (char c : text) {
if (c != ' ') {
++count;
}
}
return count;
}
int main() {
std::string order = "two loaves and a roll";
std::cout << "letters: " << count_characters(order) << '\n';
return 0;
}Without const this function could modify the caller's string. With const it cannot, and the compiler proves it. The rule of thumb for modern C++ is simple:
- Small, cheap to copy types such as
int,doubleandchar— pass by value. - Anything expensive to copy, such as
std::string, containers or user-defined types — pass by const reference. - Only when the function genuinely needs to change the caller's object — pass by a plain reference.
Seeing the copy cost
Passing a large object by value copies it, and the cost is measurable.
#include
#include
void by_value(std::string order) {
order += " (copy)";
}
void by_const_reference(const std::string& order) {
std::cout << "length seen: " << order.length() << '\n';
}
int main() {
std::string order(200, 'x');
by_value(order);
by_const_reference(order);
std::cout << "original length: " << order.length() << '\n';
return 0;
}The by-value call allocates and copies two hundred characters that it then throws away. The const reference call does neither. For a string in a loop over ten thousand rows, that difference is the gap between a fast program and a slow one.
References and arrays
Arrays are a special case. When you pass an array to a function, the array decays into a pointer to its first element, so a by-value array parameter is really a pointer parameter. The size is lost, and you usually pass the length as well.
#include
double average(const double values[], int count) {
double sum = 0.0;
for (int i = 0; i < count; ++i) {
sum += values[i];
}
return count > 0 ? sum / count : 0.0;
}
void scale(double values[], int count, double factor) {
for (int i = 0; i < count; ++i) {
values[i] *= factor;
}
}
int main() {
double prices[3] = {4.00, 6.00, 8.00};
std::cout << "average: " << average(prices, 3) << '\n';
scale(prices, 3, 1.5);
std::cout << "after scale: " << prices[0] << ", " << prices[1] << ", " << prices[2] << '\n';
return 0;
}The const array parameter cannot be modified, while the non-const one can, and both need the length because the array parameter does not carry it.
Choosing a parameter type
A quick decision table.
int,double,char,bool— by value. Copying is free.std::string,std::vector, user-defined types — by const reference.- A parameter the function must modify for the caller to see — by plain reference.
- A pointer you intend to follow — by pointer, and prefer
T* constto show the pointer itself is fixed. - An optional parameter that is expensive to copy — by const reference with a default.
Quick reference
- By value copies; changes inside the function are invisible to the caller.
- By reference aliases; changes inside the function are visible to the caller.
- A const reference avoids the copy and forbids modification — the default for large objects.
- Small cheap types should be passed by value.
- Arrays decay to pointers, so the length must be passed separately.
- The call site looks identical either way, so the parameter type is the only signal about side effects.
One more habit: choose [[const T&]] by default for any parameter you do not modify, even for small types. The cost is negligible, the intent is explicit, and the compiler then enforces that the function really is read-only.
Try it yourself: write one function that takes an order total and applies a discount, first by value and then by reference, and compare the caller's variable afterwards. Then change the by-value version to take a [[const std::string&]] order number and a plain [[double&]] total, and check that the compiler rejects any attempt to modify the order number inside the function.