Functions that change their arguments
Functions and library types12 min readLesson 4 of 5
4.1 What a parameter copy costs
A function parameter declared without & is a new object. Calling the function copies the argument into that object, and changes inside the function affect only the copy. For an int the copy is trivial. For a std::string or a std::vector, the copy allocates memory and duplicates every element.
A reference parameter avoids the copy and gives the function direct access to the caller's object. That access is useful when the function is supposed to update the argument, and it is dangerous when the function is not. A const reference keeps the no-copy benefit and removes the ability to modify.
#include <iostream>
#include <vector>
void add_copy(std::vector<int> values, int value) {
values.push_back(value);
}
void add_ref(std::vector<int>& values, int value) {
values.push_back(value);
}
int main() {
std::vector<int> scores = {70, 80};
add_copy(scores, 90);
std::cout << "After copy: " << scores.size() << "\n";
add_ref(scores, 90);
std::cout << "After ref: " << scores.size() << "\n";
return 0;
}Output
After copy: 2 After ref: 3
4.1.1 Read-only and writable parameters
Three parameter forms cover most cases:
T valuewhen the function needs its own copy orTis cheap to copy.const T& valuewhen the function reads a large object and does not change it.T& valuewhen the function changes the caller's object.
A const T& parameter accepts lvalues, literals, and temporaries. A T& parameter accepts only an lvalue that the function may modify, so f(42) fails to compile when f takes int&. That restriction is useful: a call that looks read-only cannot modify a temporary.
#include <iostream>
#include <vector>
double average(const std::vector<int>& values) {
int total = 0;
for (int value : values) {
total += value;
}
return static_cast<double>(total) / values.size();
}
int main() {
std::vector<int> marks = {70, 80, 90};
std::cout << "Average: " << average(marks) << "\n";
std::cout << "Count: " << marks.size() << "\n";
return 0;
}Output
Average: 80 Count: 3
4.1.2 Returning results through parameters
A function returns one value directly. When it must produce two or three related results, reference parameters offer a straightforward option. Names such as min_out and max_out tell the caller that the function writes through them.
The caller has to create the output variables first and pass them in the right order. A small struct with named fields is easier to read once the number of results grows, and it avoids a signature full of references. Choose the form that keeps the call site understandable.
#include <iostream>
void swap_values(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
int main() {
int left = 3;
int right = 8;
swap_values(left, right);
std::cout << "Left: " << left << "\n";
std::cout << "Right: " << right << "\n";
return 0;
}Output
Left: 8 Right: 3
4.1.3 Functions that return references
A function can return a reference when the caller should work with an existing object. A getter that returns const std::string& avoids copying a member. A function that returns int& into a container lets the caller update an element in place.
The object behind the reference must survive the call. Do not return a reference to a local variable, and do not return a reference to a temporary created in the function. Returning a reference into a vector is safe until the vector reallocates, so the caller has to know that rule as well.
#include <iostream>
#include <vector>
void min_max(const std::vector<int>& values, int& min_out, int& max_out) {
min_out = values[0];
max_out = values[0];
for (int value : values) {
if (value < min_out) min_out = value;
if (value > max_out) max_out = value;
}
}
int main() {
std::vector<int> temps = {18, 25, 12, 30, 21};
int low = 0; int high = 0;
min_max(temps, low, high);
std::cout << "Low: " << low << "\n";
std::cout << "High: " << high << "\n";
return 0;
}Output
Low: 12 High: 30
4.1.4 References and pointers in an interface
A reference parameter states that an argument is required and must be valid. A pointer parameter states that the argument is optional, because nullptr is a valid value. That difference is worth preserving in a public function.
Older C libraries use pointers for output parameters and for arrays. A modern wrapper can keep the pointer in its signature to match the original API, then check for null once at the boundary. Code inside the wrapper can use references after that check.
#include <iostream>
#include <string>
void print_badge(const std::string& name, const std::string* title) {
if (title != nullptr) {
std::cout << *title << " " << name << "\n";
} else {
std::cout << name << "\n";
}
}
int main() {
std::string doctor = "Dr.";
print_badge("Rao", &doctor);
print_badge("Iyer", nullptr);
return 0;
}Output
Dr. Rao Iyer
4.1.5 Common mistakes
- Declaring a reference without an initializer.
- Returning a reference to a local variable or to a temporary.
- Taking a non-const reference when the function only reads the argument.
- Forgetting that a reference parameter changes the caller's object, then calling the function twice.
- Keeping a reference to a vector element after the vector grows.
- Writing
int& r = other;and expectingrto refer toother. It assigns the value instead.
A function that changes an argument through a reference should say so in its name and its documentation. normalize(text) and read_config(path, settings) give a reader a hint about what happens to the arguments.
4.1.6 Practice
- Write
void scale(std::vector<int>& values, int factor)and call it on a vector. - Write
double average(const std::vector<int>& values)and confirm that the vector is unchanged. - Write a function that fills two output parameters with the smallest and largest values in a vector.
- Try to pass a literal to a function that takes
int&and read the compiler error. - Return a reference to an element of a vector, change it, and print the vector.
