Skip to content

Chapter 2 — Value Semantics

Chapter 2 — Value Semantics

The single biggest mental shift from C#. In C#, the type decides: class = reference, struct = value. In C++, everything behaves like a C# struct by default — assignment copies, passing copies, returning copies. Whether something is shared is decided at the point of use, not by the type's author.

For Java readers: no struct intuition to borrow — read it as everything assigns like a Java primitive: the whole object copied, fields and methods included. Java has no such type at all, so this is the one anchor you build from scratch; the code below is the construction kit.

class Widget {          // 'class' keyword, but behaves like a C# STRUCT
public:
    std::string name;
};

Widget a;  a.name = "first";
Widget b = a;            // FULL COPY - two independent objects
b.name = "second";
std::cout << a.name;     // still "first"

(In C++, class and struct are identical except for two defaults: member access — private vs public — and base-class access, so class D : B inherits privately where struct D : B inherits publicly. Nothing to do with copy semantics.)

You choose the semantics per variable

Widget b = a;    // copy - independent object
Widget& r = a;   // reference - alias, same object (C# class behavior)
Widget* p = &a;  // pointer - same object, can be null / reseated
auto s = std::make_shared<Widget>();  // shared ownership, closest to C# feel

Which of those four to reach for, in a parameter, a return, or a container element, is the subject of Appendix H — this chapter supplies the semantics, that page supplies the choice.

Trap 1 — accidental copies in loops

for (auto w : widgets)         // copies EVERY widget
    w.selected = true;         // modifies the copies - vector unchanged!

for (auto& w : widgets)        // reference - the fix
    w.selected = true;

for (const auto& w : widgets)  // read-only pass - the idiom for viewing
    std::cout << w.name;

Trap: The missing & is dead silent — compiles, runs, does nothing. One of the most common real-world C++ bugs.

Try it (30 seconds). void AddOne(std::vector<int> v) { v.push_back(1); } — call it and predict the caller's v.size(). Then add the & and predict again. The first answer is this Trap; the second is the fix.

Trap 2 — modifying a copy returned from a function

Widget GetSelected() { return selected_; }   // returns a COPY
GetSelected().name = "new";  // edits a temporary that dies instantly. No-op.

Trap 3 — object slicing (the nastiest)

class Shape  { public: virtual void Draw(); int x, y;
               virtual ~Shape() = default; };   // Chapter 5: always on a base
class Circle : public Shape { public: void Draw() override; int radius; };

Circle c;
Shape s = c;          // COPIES only the Shape part. radius is GONE.
s.Draw();             // calls Shape::Draw - polymorphism lost!

std::vector<Shape> shapes;
shapes.push_back(c);  // sliced again

Polymorphism in C++ therefore requires pointers or references:

Shape& r = c;
r.Draw();             // Circle::Draw - virtual dispatch works

std::vector<std::unique_ptr<Shape>> shapes;   // correct polymorphic container
shapes.push_back(std::make_unique<Circle>());
shapes[0]->Draw();    // Circle::Draw

That virtual ~Shape() = default; in the declaration is load-bearing, not boilerplate: each unique_ptr<Shape> deletes through a Shape*, and without it ~Circle never runs. Chapter 5 gives that rule its own page — it appears here already because the container above would be undefined behavior without it.

Rule: value types for data, pointers/references for polymorphism. In C# every class object lives behind a reference automatically, so slicing cannot happen; in C++ you must ask for reference behavior.

Why C++ is built this way — the payoff

Value semantics means objects live on the stack or inline inside containers — contiguous memory, no GC pressure, no pointer-chasing. A std::vector<Point> of a million points is one solid block of memory, cache-friendly and fast. The equivalent List<Point> with a Point class in C# is a million scattered heap objects. This is a big part of why C++ is the language of CAD engines — geometry code lives and dies by this. Copies also mean isolation: a function taking Widget by value cannot cause spooky action at a distance.

In the wild: C-style SDKs

SDK structs are typically plain value types — created on the stack, zeroed with = {}, address passed to API functions to fill in. No heap, no ownership questions. But some structs contain pointers to SDK-allocated data (like Chapter 17's ThingData.values) — those need the RAII treatment from Chapter 1. Reading a vendor header and classifying each struct — pure value, or value-with-owned-payload? — is a daily skill in SDK work.

Key principle: "C++ is value-semantic by default; I opt into reference semantics explicitly." | "I iterate with const auto& to avoid accidental copies." | "Polymorphic objects go behind unique_ptr — storing them by value slices them."