Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Struct Parameter and Return Techniques
Pass structs to functions by value or reference and return them from functions.
Passing and returning structs
Consider a temperature sensor represented by 3 separate variables:
int main()
{
int deviceId { 201 };
double celsius { 23.5 };
int signalStrength { 85 };
return 0;
}
If we want to pass this sensor data to a function, we must pass three separate variables:
#include <iostream>
void printSensorData(int deviceId, double celsius, int signalStrength)
{
std::cout << "Device: " << deviceId << '\n';
std::cout << "Temp: " << celsius << " C\n";
std::cout << "Signal: " << signalStrength << "%\n";
}
int main()
{
int deviceId { 201 };
double celsius { 23.5 };
int signalStrength { 85 };
printSensorData(deviceId, celsius, signalStrength);
return 0;
}
While passing 3 individual variables isn't terrible, imagine a function requiring 10 or 12 sensor attributes. Passing each variable independently becomes time-consuming and error-prone. Additionally, if we add a new attribute to our sensor data (e.g., humidity), we must modify all function declarations, definitions, and calls to accept the new parameter and argument!
Passing structs (by reference)
A major advantage of using structs over individual variables is passing the entire struct to functions that need to work with the members. Structs are generally passed by reference (typically by const reference) to avoid making copies.
#include <iostream>
struct TemperatureSensor
{
int deviceId {};
double celsius {};
int signalStrength {};
};
void printSensorData(const TemperatureSensor& sensor) // note pass by reference here
{
std::cout << "Device: " << sensor.deviceId << '\n';
std::cout << "Temp: " << sensor.celsius << " C\n";
std::cout << "Signal: " << sensor.signalStrength << "%\n";
}
int main()
{
TemperatureSensor outdoor { 201, 15.2, 78 };
TemperatureSensor indoor { 202, 22.8, 92 };
// Print outdoor sensor data
printSensorData(outdoor);
std::cout << '\n';
// Print indoor sensor data
printSensorData(indoor);
return 0;
}
In this example, we pass an entire TemperatureSensor to printSensorData() (twice, once for outdoor and once for indoor).
The above program outputs:
Device: 201 Temp: 15.2 C Signal: 78%
Device: 202 Temp: 22.8 C Signal: 92%
Because we're passing the entire struct object (rather than individual members), we only need one parameter regardless of how many members the struct object has. And in the future, if we add new members to our TemperatureSensor struct, we won't need to change the function declaration or function call! The new member is automatically included.
We discuss when to pass structs by value vs reference in lesson 12.6.
In the prior example, we created TemperatureSensor variable outdoor before passing it to the printSensorData() function. This allows us to give the variable a name, which can be useful for documentation. But it also requires two statements (one to create outdoor, one to use outdoor).
In cases where we only use a variable once, having to give it a name and separate creation from use can increase complexity. In such cases, using a temporary object may be preferable. A temporary object is not a variable, so it has no identifier.
Here's the same example, but we've replaced variables outdoor and indoor with temporary objects:
#include <iostream>
struct TemperatureSensor
{
int deviceId {};
double celsius {};
int signalStrength {};
};
void printSensorData(const TemperatureSensor& sensor) // note pass by reference here
{
std::cout << "Device: " << sensor.deviceId << '\n';
std::cout << "Temp: " << sensor.celsius << " C\n";
std::cout << "Signal: " << sensor.signalStrength << "%\n";
}
int main()
{
// Print outdoor sensor data
printSensorData(TemperatureSensor { 201, 15.2, 78 }); // construct a temporary TemperatureSensor to pass to function (type explicitly specified) (preferred)
std::cout << '\n';
// Print indoor sensor data
printSensorData({ 202, 22.8, 92 }); // construct a temporary TemperatureSensor to pass to function (type deduced from parameter)
return 0;
}
We can create a temporary TemperatureSensor in two ways. In the first call, we use syntax TemperatureSensor { 201, 15.2, 78 }. This tells the compiler to create a TemperatureSensor object and initialize it with the provided initializers. This is the preferred syntax because it clearly indicates what kind of temporary object we're creating, and there's no way for the compiler to misinterpret our intentions.
In the second call, we use syntax { 202, 22.8, 92 }. The compiler is smart enough to understand the provided arguments must be converted to a TemperatureSensor so the function call succeeds. Note that this form is considered an implicit conversion, so it won't work in cases where only explicit conversions are acceptable.
We discuss class type temporary objects and conversions more in lesson 14.13.
A few more things about temporary objects: they are created and initialized at the point of definition, and are destroyed at the end of the full expression in which they're created. And evaluation of a temporary object is an rvalue expression, so it can only be used where rvalues are accepted. When a temporary object is used as a function argument, it only binds to parameters that accept rvalues. This includes pass by value and pass by const reference, and excludes pass by non-const reference and pass by address.
Consider a case where a function needs to return GPS coordinates. A GPS position has 3 attributes: latitude, longitude, and altitude. But functions can only return one value. So how do we return all 3 coordinates to the caller?
One common approach is to return a struct:
#include <iostream>
struct GpsCoordinate
{
double latitude { 0.0 };
double longitude { 0.0 };
double altitude { 0.0 };
};
GpsCoordinate getOriginPoint()
{
// We can create a variable and return it (we'll improve this below)
GpsCoordinate temp { 0.0, 0.0, 0.0 };
return temp;
}
int main()
{
GpsCoordinate origin { getOriginPoint() };
if (origin.latitude == 0.0 && origin.longitude == 0.0 && origin.altitude == 0.0)
std::cout << "At origin point\n";
else
std::cout << "Not at origin point\n";
return 0;
}
This prints:
At origin point
Structs defined inside functions are usually returned by value, to avoid returning a dangling reference.
In the getOriginPoint() function above, we create a new named object (temp) just to return it:
GpsCoordinate getOriginPoint()
{
// We can create a variable and return it (we'll improve this below)
GpsCoordinate temp { 0.0, 0.0, 0.0 };
return temp;
}
The object name (temp) doesn't provide any documentation value here.
We can make our function cleaner by returning a temporary (unnamed/anonymous) object instead:
GpsCoordinate getOriginPoint()
{
return GpsCoordinate { 0.0, 0.0, 0.0 }; // return an unnamed GpsCoordinate
}
In this case, a temporary GpsCoordinate is constructed, copied back to the caller, and then destroyed at the end of the expression. Note how much cleaner this is (one line vs two, and no need to understand whether temp is used more than once).
We discuss anonymous objects in more detail in lesson 14.13.
When the function has an explicit return type (e.g., GpsCoordinate), we can even omit the type in the return statement:
GpsCoordinate getOriginPoint()
{
// We already specified the type at the function declaration
// so we don't need to do so here again
return { 0.0, 0.0, 0.0 }; // return an unnamed GpsCoordinate
}
This is considered an implicit conversion.
Also note that since we're returning all zero values, we can use empty braces to return a value-initialized GpsCoordinate:
GpsCoordinate getOriginPoint()
{
// We can use empty curly braces to value-initialize all members
return {};
}
Structs are an important building block
While structs are useful in themselves, classes (which are the heart of C++ and object-oriented programming) build directly on the concepts we've introduced here. Having a solid understanding of structs (especially data members, member selection, and default member initialization) will make your transition to classes much easier.
Summary
Passing structs by reference: Instead of passing multiple individual variables to functions, pass the entire struct by const reference to avoid copying. This is cleaner, more efficient, and automatically includes new members added to the struct without requiring function signature changes.
Temporary struct objects: Create temporary (unnamed) structs directly in function calls using either explicit type syntax (TemperatureSensor { 201, 15.2, 78 }) or implicit conversion ({ 201, 15.2, 78 }). Temporaries are destroyed at the end of the full expression, making them ideal for single-use values.
Returning structs: Functions can return structs by value to effectively return multiple values. This solves the limitation that functions can only return one value, since that one value can be a struct containing multiple members.
Returning temporary structs: Return unnamed temporary structs using return TemperatureSensor { ... }; or simply return { ... }; when the return type is explicit. This is cleaner than creating a named local variable just to return it.
Return type deduction: When a function has an explicit return type, the type in the return statement can be omitted and the compiler will deduce it. For structs with all zero values, return {}; returns a value-initialized instance.
Temporary object lifetime: Temporary objects bind to const references and values, but not to non-const references. They exist only until the end of the full expression in which they're created, so they're safe for immediate use but dangerous to store references to.
Why structs matter: Structs bundle related data together, making functions easier to call, parameters easier to manage, and code easier to maintain. They form the conceptual foundation for classes and object-oriented programming.
Understanding how to pass and return structs effectively is essential for writing clean, maintainable C++ code.
Struct Parameter and Return Techniques - Quiz
Test your understanding of the lesson.
Practice Exercises
Vector Math Operations
Create a program that works with 2D vectors using structs. Implement functions to add vectors and calculate magnitude, demonstrating passing structs by const reference and returning structs by value.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!