TRT
Sign in

Foundations · Module 4 · Lesson 3 of 9

Passing, Returning and Pointing to Structs

Compare passing a struct by value, by reference and by pointer, learn what the arrow operator does, and see why returning a struct by value is usually free thanks to copy elision.

FreeAbout 8 minutes

By the end you can

  • Predict whether a function changes the caller's struct for value, reference and pointer parameters
  • Use the dot and arrow operators correctly and say what the arrow means
  • Explain why returning a struct by value is usually cheap
  • Choose the parameter form for a struct of a given size and role

Builds on Structs: Grouping Related Data, Pointers: Variables That Hold Addresses, Passing by Value, by Reference and by Const Reference.

01 · A question first

Handing a struct to a function can copy dozens of bytes, share the original, or pass only its address, and returning one looks like yet another copy. Which form should you pick, what does the arrow operator really do, and why is returning a struct by value usually free?

Make a guess, then step through the trace below to check it.

02 · Watch it run

One struct, three ways to pass itstep 1 of 5
struct Point { int x; int y; };void moveByValue(Point p) { p.x = p.x + 1; }void moveByRef(Point& p) { p.x = p.x + 1; }void moveByPtr(Point* p) { p->x = p->x + 1; }Point makePoint(int x, int y) { return {x, y}; }Point a = makePoint(1, 2);moveByValue(a);moveByRef(a);moveByPtr(&a);
  1. item 0a.x: 1(looked at in this step)
  2. item 1a.y: 2(looked at in this step)
  3. item 2param: --

In Point a = makePoint(1, 2), the caller reserves 8 bytes for a and passes their address to makePoint as a hidden argument. The function builds its result directly in that storage. Since C++17 this is guaranteed: there is no temporary and no copy. The cells show a.x, a.y and the parameter of whichever function is running.

Machine work: hidden argument: the address of a; construct (1, 2) in place

03 · The idea

Start with the problem

Lesson 7 of module 2 gave three ways to pass a value, and lessons 6 and 8 of module 3 added pointers. A struct makes the choice matter. A struct can be much larger than an int, so copying it is no longer free, and the same struct may need to be handed to a function, modified there, or returned from it.

Returning one raises its own worry. If a function builds a struct and returns it by value, does the whole thing get copied back? And when all you have is a pointer to a struct, how do you reach its members? This lesson answers both.

How the idea works

By value. The parameter is a copy of all the members, as steps 2 and 3 of the trace show. The function can change its copy freely and the caller never notices. The cost is the size of the struct.

By reference. Step 4 shows Point& p. There is no copy, and p is another name for the caller's object, so changes are visible. For a function that only reads, const Point& avoids the copy and forbids modification:

cpp
double distance(const Point& a, const Point& b);

By pointer. The caller passes &a, the address, which is 8 bytes (step 5). Inside the function, the members are reached with the arrow operator. The expression p->x is shorthand for (*p).x: follow the pointer, then take the member x. The arrow exists because writing *p.x would mean *(p.x), as the dot binds tighter than the star. The rule is simple: use the dot on objects and references, and the arrow on pointers. A pointer can be null, so check it first, as lesson 8 of module 3 explained.

Returning by value. A function can return a struct as it returns an int:

cpp
Point makePoint(int x, int y) {
    return {x, y};
}

The trace shows the first step of this. The compiler has the caller reserve the storage for the result and pass its address to the function as a hidden argument. The function then builds the struct right there. Structs that are small and simple, up to 16 bytes on common platforms, even come back in registers. Since C++17 the language guarantees that returning a temporary builds it directly in the caller's variable, which is called copy elision: no second object exists, so nothing is copied. For a named local variable, the compiler is allowed to apply the same trick and nearly always does.

Several results. A function that needs to produce more than one value can return a struct, instead of using output parameters. C++17 lets the caller unpack it with a structured binding: auto [lo, hi] = minMax(v);.

Choosing. As a rule: small structs without heap members, up to about 16 bytes, go by value. Larger ones, or ones that hold strings and vectors, go by const T&. A function that must modify its argument takes T&. A function that accepts "no object" takes T*. And functions return structs by value.

Collections. In a loop over a vector of structs, write for (const Contact& c : contacts). The form for (Contact c : contacts) copies every element.

Why it is correct

Each form keeps its own promise. A value copy is independent. A reference is the same object under another name, and a pointer is the same object reached through an address that may be null or may change. The arrow is only a notation, so that a pointer to a struct can be used with the same readability as the struct itself.

Guaranteed copy elision works because the language defines the returned object to be the caller's object. That leaves the meaning of the program unchanged, so the result can even be a type that cannot be copied at all, and the performance does not depend on the optimizer's mood.

What it costs

Passing by value. The cost is the size of the struct. For a Point of 8 bytes it is trivial. For the 48-byte Contact of lesson 1 it is 48 bytes plus a copy of the string, which allocates when the name is longer than the small-string buffer, typically 100 nanoseconds. A reference or a pointer costs 8 bytes whatever the size.

Returning by value. Usually nothing. Elision or registers remove the copy, and a function that returns a Point and is called a hundred million times shows no overhead once it is inlined.

Indirection. A reference or a pointer adds one load when the member is used, and it stops the compiler from assuming that two names refer to different objects.

Copying in loops. A range-based loop that copies each element of a vector of a million contacts with long names performs a million allocations.

Pitfalls and variations

Expecting a by-value change to reach the caller. Steps 2 and 3 show why it does not.

Big structs by value. Passing large structs by value in hot code, or looping with auto c instead of const auto& c, is a common hidden cost.

Dot versus arrow. Using the dot on a pointer gives an error such as request for member 'x' in 'p', which is of pointer type 'Point*' (maybe you meant to use '->' ?). The compiler tells you what to do.

The precedence trap. *p.x is not (*p).x. Use the arrow.

Null and dangling. The arrow on a null pointer is undefined behavior, and so is the arrow on a pointer to a struct that no longer exists, as in lesson 10 of module 3.

Returning references to locals. Return by value instead.

Stored pointers. A pointer to an element of a vector dangles after the vector reallocates, as in lesson 4 of module 3.

Variations. std::pair and std::tuple return several values without a named struct. std::optional<Point> returns "a point or nothing". The pattern bool parse(const std::string& text, Point& out) returns success and fills an output parameter, which is common when failure is expected. C++23's std::expected returns a value or an error.

Where it is used

Graphics and math libraries pass vectors as const Vec3& and return them by value. C interfaces take a pointer to a struct that the function fills in, as stat(path, &info) does. Network code passes pointers to address structures. And the arrow operator is the everyday way of using objects through pointers, which later modules rely on at every step.

The next lesson, "Enumerations: enum and enum class", gives names to a small set of values, which is the other half of describing your data with types.

04 · Prove it

Question 1 of 4 · answer from memory

`void f(Point p) { p.x = 99; }` is called as `f(a);` where `a` is a `Point` with `a.x` equal to 1. What is `a.x` afterwards?

05 · Practice

Something wrong with this lesson? Report a problem

Not started

Sign in to save your learning progress.

Sign in to save