Skip to content
VLSI Mentor

C++ Part I · Module 9 · Arrays, Strings, and Aggregate Data

Choosing a Representation for Engineering Data

This module has spent ten chapters on tools that overlap on purpose. A fixed table of numbers can be a built-in array. A piece of text can be a char array, a std::string, or a std::string_view, and all three arrived with measurements arguing for a different one. A register description can be three loose variables, a struct, or a struct with an enum class member. Every one of those chapters answered how the thing works; not one of them answered which to reach for when a real description is in front of you, and that gap is where most bad data design comes from — not from ignorance of the features, but from picking whichever was learned most recently. This chapter is the decision, and it introduces no new syntax. It is six questions asked about the data rather than about the language, in an order that matters, each answered with a measurement this module already made: how many values and is the count known when you compile, who owns the bytes, does it cross a function boundary, is it text, do the values travel together, and what has to stay true. The worked example is a signal table from a real flow, and it ends honestly — one part of it cannot be represented with anything in this module, which is precisely why the next one exists.

Intermediate19 min readC++Part IFundamentalsDesign

Part I · Module 9 · Chapter 9.11 · Choosing a Representation for Engineering Data

1. What This Chapter Is and Is Not

It is not new syntax. Everything here appeared in Chapters 9.1 to 9.10.

It is not a style guide. Every claim is backed by a measurement from this module, re-cited with its number.

It is a decision procedure — the same kind Chapter 6.9 built for function interfaces, applied to data instead. Chapter 6.9 §2 argued that a signature is a contract; the claim here is the matching one:

A type is a claim about what the data is. Choosing badly means writing a claim that is false, and then defending it by hand for the rest of the program's life.

The reason this needs a chapter is that the features overlap, so knowing them all is not sufficient. Chapters 9.4, 9.5 and 9.6 all represent text and gave different measurements. Something has to decide, and the thing that decides should be the data, not your familiarity.

2. The Six Questions, In Order

The order is not arbitrary — each answer narrows what the next one can be.

#QuestionWhat it decides
1How many values, and is the count known when you compile?sequence or single; array or Module 10
2Who owns the bytes?owning or viewing
3Does it cross a function boundary?whether decay or a copy is about to happen
4Is it text?char[], std::string, or std::string_view
5Do the values travel together?loose variables or a struct
6What has to stay true about it?aggregate, or a class with an invariant

Questions 1 to 4 are mostly mechanical and this module measured all of them. Question 5 is judgement. Question 6 is the one that hands over to Module 11, and §6 explains why it is last.

3. Question 1 — How Many, and Known When?

Three cases, and only two of them belong to this module.

One value. Use the type of the value. Nothing in this module applies.

A fixed count, known at compile time. A built-in array is correct and Chapter 9.1 measured why it is cheap: sizeof(delays) was 16 for four ints, sizeof(delays[0]) was 4, and the elements sat at consecutive addresses four bytes apart. There is no indirection and no allocation — the values are the object.

A count not known until the program runs. Nothing in this module can do this honestly, and §8 is where that bites. Module 10 is the answer.

The compile-time case comes with a caveat Chapter 9.1 measured precisely, and it is the main argument against built-in arrays:

How the out-of-range index is writtenDiagnosed?
a literal — ai[9] on int[4]caught — warning: array index 9 is past the end of the array (that has type 'int[4]') [-Warray-bounds]
a runtime valuesilent; ASan reported stack-buffer-overflow
through a pointersilent

The reason is one sentence from Chapter 9.1: an array's type carries its length and a pointer's does not. Which leads directly to question 3.

4. Question 2 — Who Owns the Bytes?

This is the question most often skipped, and skipping it is what produces dangling bugs.

Owning means the object is responsible for the bytes: they live as long as it does, and copying it copies them. std::string owns; a built-in array owns; a struct owns its members.

Viewing means the object refers to bytes that something else is responsible for. std::string_view views; a pointer views; a reference views.

Chapter 9.6 measured the difference as addresses rather than as a definition:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
caller's data at 0x102ae99a0
  std::string  param data at 0x102ae99e0   <- DIFFERENT: copied
  string_view  param data at 0x102ae99a0   <- IDENTICAL: no copy

A view is cheap because it is not responsible for anything, and that is exactly why it can outlive what it views. Chapter 9.6 §5's measurement is the cost of answering this question wrong.

The rule that follows: a view is correct for a parameter, a local, or anything whose lifetime is visibly shorter than the thing it refers to. A member of a struct that is stored somewhere should own, because you generally cannot see what will outlive what.

5. Question 3 — Does It Cross a Function Boundary?

For arrays this is the question that changes the answer, because of the measurement in Chapter 9.2:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
in main, sizeof(delays) = 16 bytes
  inside, sizeof(a) = 8 bytes      (parameter declared int a[])
  inside, sizeof(p) = 8 bytes      (parameter declared int* p)

The length is gone, both parameter forms are the same thing, and Chapter 9.2 also measured that void f(int a[100]) cheerfully accepted a one-element array. The 100 documents nothing and checks nothing.

So an array that never leaves the function it was declared in is fine. An array that is passed somewhere has lost the only thing that made it self-describing, and you are back to passing a length by hand — which Chapter 9.2 showed is unchecked, because sum_with_length(delays, 400) on a four-element array produced zero diagnostics.

There is a fix, and it is a representation decision rather than a new feature.

6. Wrapping Restores What Decay Removes

A built-in array decays. A struct containing one does not. Measured:

Azvya Education Pvt. Ltd.VLSI Mentor
wrap.cpp — the relevant declarations
struct Bus { int bits[4]; };

void takesArray(int bits[4]);        // decays
void takesStruct(Bus b);             // does not
void takesStructRef(const Bus& b);   // does not
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
in main: sizeof(raw) = 16   sizeof(Bus) = 16
  bare array param   : sizeof = 8
  after takesArray,  raw[0]      = 99      <- the CALLER'S data changed
  struct param        : sizeof = 16
  after takesStruct, bus.bits[0] = 1       <- the caller's data did NOT
  const Bus& param    : sizeof = 16
  after copying the struct and writing, bus.bits[1] = 2   copy.bits[1] = 77
  struct assignment other = bus works, other.bits[2] = 3

(That program produces one warning, and the warning is the point: it is Chapter 9.2's -Wsizeof-array-argument, fired on the bare-array parameter and not on either struct form.)

Four things changed by wrapping, and nothing else did — sizeof(Bus) is 16, the same bytes as the array:

The length survives the call. sizeof inside the function is 16, not 8.

It is passed by value. Writing through the bare-array parameter set the caller's raw[0] to 99; writing through the struct parameter left bus.bits[0] at 1.

It can be copied. Bus copy = bus; duplicated the elements — 2 and 77 afterwards.

It can be assigned, which a bare array refuses outright:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int b[4]; b = a;    error: array type 'int[4]' is not assignable
Bus  b    = a;      0 errors

A built-in array is the one C++ type that does not behave like a value. Putting it in a struct makes it behave like one again.

That is worth understanding as a mechanism rather than a trick, because it is what std::array is: a struct wrapping an array, with the length in the type. Chapter 19.2 is where you meet it, and once you have it you will prefer it to both forms above. The reason to see it this way first is that it explains why std::array is not merely a nicer array — it is a different category of thing, and the category change is the product.

And the const Bus& row is Chapter 6.5's rule arriving unchanged: 16 bytes, no decay, no copy.

7. Question 4 — Is It Text?

Three representations, all measured, and the decision is nearly mechanical.

SituationUseEvidence
a parameter you only readstd::string_view0 allocations in all five source cases (9.6)
text you store or modifystd::stringowns, grows, stays terminated (9.5)
a fixed literal at file scopeconst char[] or string_viewno allocation either way
an interface you do not controlconst char*, converted at the edge9.4

Chapter 9.6's full allocation count is the argument for the first row — a const std::string& parameter allocated in four of five cases (from a literal, a const char*, a char array, and a substr, but not from an existing std::string), while a string_view parameter allocated in none of the five.

With the qualification Chapter 9.6 had to issue and that belongs in any honest summary: that gap only appears past the inline capacity, measured at 22 characters in Chapter 9.5. With short text the counts were 0 either way. The decision still goes the same way — string_view is never worse — but if you measure it with a four-character test string you will see nothing and conclude wrongly.

And C strings are a conversion target, not a representation. Chapter 9.4 measured what the two numbers cost: sizeof(greeting) 5 against strlen(greeting) 4, and an unterminated array handed to a stream produced an ASan stack-buffer-overflow reported from inside __constexpr_strlen — the library scanning for a terminator that was not there. Nothing about that is a reason to choose char[] for new data. It is a reason to know it, because the interfaces you must call still use it.

Two library types that belong to this question and to Part II: std::optional for a value that may be absent, and std::variant for a value that is one of several types. Both are better than the usual improvisations — a sentinel value, or a struct with a bool and a union. Named here so you know the question has a real answer.

8. Question 5 — Do the Values Travel Together?

This one is judgement, and the test is operational rather than aesthetic:

If you find yourself passing two or three variables to the same functions, in the same order, every time — they are one thing, and the language should know it.

Chapter 9.7 §2's argument, with Chapter 9.8's measurement behind it. A struct gets you: one parameter instead of three, a name for the concept, members that cannot be reordered by accident at a call site, and return-by-value of the group (Chapter 6.3).

It costs you two things worth knowing, both measured in Chapter 9.8:

Padding. sizeof is not the sum of the members. Measured: four members totalling ten bytes gave 16 bytes in one order and 12 in another — and the fix is free, which is to declare the widest members first.

Positional initialization. Field{4, 8} and Field{8, 4} are both accepted when both members are int. Which is what question 6 is for.

9. Question 6 — What Has to Stay True?

Last, because it is the question that decides whether this module is enough.

Every representation carries statements that must remain true: a width is positive, an offset plus a width fits inside a register, an access mode is one of three values, a name is not empty. Those are invariants, and the choice is where they are enforced.

Some invariants a type can enforce by itself, and this module built two:

enum class makes "the mode is one of three values" unrepresentable otherwise. Chapter 9.9 measured three operations it refuses that a plain enum permits silently — and crucially, it also fixes Chapter 9.8's positional hazard for that member, because an Access cannot be swapped with an int.

std::string makes "the length agrees with the characters" automatic, where Chapter 9.4 showed char[] leaves it to you.

Some it cannot. Nothing in this module stops Field{40, 100} from claiming a 100-bit field at offset 40 in a 32-bit register. A struct is an aggregate — Chapter 9.8 §2's definition is exactly that it has no constructor deciding how it is built, which is also why Chapter 9.10 §4 found that private members cannot be decomposed. An aggregate is transparent by design, and transparency and enforcement are opposites.

That is the limit of Module 9, and it is the question Module 11 opens: Chapter 11.1 is titled Why Classes Exist: Invariants, Not Just Grouping, and Chapter 11.5 is where encapsulation makes an invariant enforceable rather than hoped for.

So the practical rule for now: use an aggregate when the members are independent facts, and recognise when you are maintaining a rule by hand — that recognition is the thing to carry into Module 11.

10. A Worked Example: A Signal Table

From a real flow. The description:

Read a signal list at startup. Each signal has a name, a bit width, and an access mode of read-only, read-write or write-one-to-clear. The number of signals is whatever is in the file. Print a report, and look signals up by name.

A first attempt, written by someone who knows the features but has not asked the questions:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    char  names[256][64];        // 256 signals max, 64 chars max
    int   widths[256];
    int   modes[256];            // 0, 1 or 2
    int   count = 0;

Everything in it is legal and every one of the six questions was answered wrongly.

Question 1 — how many, known when? Not known until the file is read, and 256 is a guess that is simultaneously too small and wasteful. Chapter 9.2's measurement applies the moment any of these is passed anywhere: the length disappears and count has to travel alongside, unchecked.

Question 2 — who owns? Fine, by accident.

Question 3 — crossing a boundary? Yes, and all four arrays decay. A lookup function needs five parameters.

Question 4 — text? char[64] means a 64-character limit nobody checked and Chapter 9.4's terminator problem on every write.

Question 5 — travel together? They always travel together, and the representation says the opposite. Worse: nothing ties index i of one array to index i of another. Sort one and the data is silently destroyed — a bug with no diagnostic at all.

Question 6 — what must stay true? That modes[i] is 0, 1 or 2. It is an int, so Chapter 9.9's three silent operations are all available.

The same description, with the questions answered:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    enum class Access : std::uint8_t { ReadOnly, ReadWrite, WriteOneToClear };

    struct Signal {
        std::string name;        // owns; no length limit; stays terminated
        int         width;
        Access      access;      // three values, not four billion
    };

    // one signal, not three parallel arrays
    void report(const Signal& s);                   // 6.5: no copy
    const Signal* find(/* the sequence */, std::string_view wanted);

What each change bought, in terms already measured:

ChangeMeasured consequence
std::string for the nameno 64-char limit, no terminator to maintain (9.4, 9.5)
enum class Accessthree refusals instead of three silent bugs (9.9)
one structindex i of three arrays can no longer disagree (9.7)
widest member firstChapter 9.8's 16-vs-12 rule, free
const Signal& parameterno copy of the struct or its string (6.5, 9.6)
std::string_view wanted0 allocations for a literal argument (9.6)

And the part this module cannot do. Look at what find takes. The sequence — a number of Signal objects that is not known until the file is read. Module 9's only sequence is the built-in array, and a built-in array needs its length at compile time. The honest options available right now are a fixed maximum with a count (the guess that was wrong above) or new[], which is Module 10 and brings every problem Module 10 is about.

The representation is right. The container is missing, and nothing in this module can supply it.

11. What to Actually Do

Ask what the data is before asking which feature to use. The six questions of §2, in that order.

Use a built-in array only for a compile-time count that does not leave the function. Chapter 9.2's measurement is why the second half matters.

Wrap an array in a struct when it must cross a boundary — and prefer std::array once Chapter 19.2 gives it to you.

Use std::string to store text and std::string_view to read it. Reach for char[] only at an interface you do not control.

Group values that always travel together into a struct, and declare the widest members first.

Use enum class for any member that is a fixed set of choices. It removes a whole class of silent bug for that member.

Ask who owns the bytes before storing anything view-like. A view as a parameter is right; a view as a stored member needs an argument.

Notice when you are maintaining an invariant by hand. That is the signal that you have reached the edge of an aggregate, and it is what Module 11 is for.

Do not choose a container before you know what the element is. Chapter 19.7 is a separate decision and it comes second.

12. Mistakes Worth Making Once

Parallel arrays. names[i], widths[i], modes[i] — one struct instead. Nothing ties the indices together, and the failure is silent.

A fixed maximum with a count, where the count is not known at compile time. It is both a limit and a waste, and the count travels unchecked (9.2).

int for a fixed set of choices. Chapter 9.9's three operations are all available and all silent.

char[N] for a name. A limit nobody checks and a terminator to maintain (9.4).

const std::string& for a read-only parameter. Measured: it allocated in four of five source cases (9.6).

Measuring the text decision with a short string. Below the 22-character inline capacity every count is 0 and you conclude the wrong thing.

Passing a built-in array and using sizeof inside. 16 becomes 8, and the compiler does warn about exactly this (9.2).

Declaring struct members in the order they occurred to you. Measured: 16 bytes versus 12 for the same four members (9.8).

Storing a std::string_view as a member without an argument about what outlives what (9.6 §5).

Expecting an aggregate to enforce a rule. It cannot — that is Chapter 9.8 §2's definition, and Module 11's subject.

13. Interview Relevance

"How would you represent this?" — given a description, usually of a register map, a packet, or a table of signals. This is one of the most common open-ended questions for engineering C++ roles and there is no single right answer; what is being assessed is whether you ask about the data. How many, known when, who owns it, does it cross a boundary, what must stay true. A candidate who names a type immediately has answered a different question.

"Why not parallel arrays?" Because nothing connects index i across them, so any reordering silently corrupts the data, and because each one decays separately when passed. A struct makes the connection a property of the type.

"std::string or std::string_view for this parameter?" string_view if you only read it. The strong answer includes that the gap is real only past the small-string capacity — that qualification is what separates someone who measured it from someone who read a guideline.

"Why is sizeof a struct not the sum of its members?" Alignment and padding, with the reordering consequence (9.8). Very common.

"When would you use a plain array in new code?" A fixed, compile-time count, local to a function, where you want no indirection. Being able to say when the built-in array is still right is a better signal than reflexively answering std::vector.

14. What You Should Be Able to Say Now

  1. A type is a claim about what the data is — choosing badly means defending a false claim by hand.
  2. The six questions, in order: how many and known when; who owns; does it cross a boundary; is it text; do the values travel together; what must stay true.
  3. A built-in array is right for a compile-time count that stays local — measured cheap in 9.1, and bounds-checked only for a literal index.
  4. Measured: passing it loses the length — 16 becomes 8 — and void f(int a[100]) accepted a one-element array.
  5. Wrapping an array in a struct restores value semantics: measured, sizeof stayed 16, the caller's data was not modified, and copy and assignment both worked where a bare array gives array type 'int[4]' is not assignable.
  6. That is what std::array is — a category change, not a nicer array (19.2).
  7. Owning versus viewing is the question that prevents dangling bugs; 9.6's three addresses are the demonstration.
  8. std::string_view to read, std::string to store — 0 of 5 allocations against 4 of 5, past the 22-character inline capacity.
  9. C strings are a conversion target, not a representation — 9.4's sizeof 5 against strlen 4, and the ASan report from inside strlen.
  10. Values passed to the same functions in the same order every time are one thing — use a struct.
  11. A struct costs padding (16 against 12, fixed for free by ordering) and positional initialization.
  12. enum class makes an invalid mode unrepresentable and fixes 9.8's swap hazard for that member.
  13. An aggregate cannot enforce an invariant — that is its definition, and Module 11's subject.
  14. Module 9 cannot represent a runtime-sized sequence at all, which is what Module 10 and then Chapter 19.2 are for.
  15. Choosing a representation and choosing a container are different decisions — this chapter and Chapter 19.7 — and the representation comes first.

15. Exercises

Exercise 1 — the six questions. A description: "a fixed 8-entry interrupt vector table, each entry a 32-bit handler address, used only inside the scheduler." Answer all six questions and give the declaration.

Exercise 2 — critique. char names[256][64]; int widths[256]; int modes[256]; int count; — list every defect by question number, and say which one has no diagnostic of any kind.

Exercise 3 — the wrap. Given void process(int samples[64]);, explain what sizeof(samples) is inside and why, then rewrite it so the length survives the call. State what else changes about the call.

Exercise 4 — text. For each, choose a representation and justify it with a measurement: (a) a parameter that is only printed (b) a name stored in a struct kept in a table (c) a fixed set of error strings at file scope (d) a value returned by a C library.

Exercise 5 — the qualification. A colleague tests const std::string& against std::string_view and measures zero allocations for both, concluding it does not matter. Say what is wrong with the test and what to change.

Exercise 6 — invariant or not. Which can Module 9's tools enforce, and which need Module 11: (a) the mode is one of three values (b) the width is between 1 and 32 (c) the name is not empty (d) offset plus width is at most 32 (e) the string's length matches its characters.

Exercise 7 — the missing piece. For the signal table in §10, say precisely why a built-in array cannot hold the signals, and what property std::string has that makes it the exception in this module.

Exercise 8 — order the decisions. A colleague opens with "should this be a map or a vector?" before the element type exists. Explain which decision comes first and why the other way round goes wrong.

16. Summary

  • A type is a claim about the data; the six questions of §2 are how to make a true one.
  • How many, and known when decides between an array and Module 10 — and 9.1's bounds measurement is the array's main cost.
  • Measured: passing an array loses its length (16 → 8), and a declared bound is not checked.
  • Wrapping an array in a struct restores value copy, assignment and sizeof — measured — and that is what std::array is.
  • Owning versus viewing is the dangling-bug question; string_view to read, std::string to store, 0-of-5 against 4-of-5 allocations past the 22-character capacity.
  • C strings are a conversion target.
  • Values that always travel together are one struct — at the cost of padding (16 vs 12, fixed by ordering) and positional initialization.
  • enum class makes an invalid value unrepresentable — three refusals instead of three silent bugs.
  • An aggregate cannot enforce an invariant, by definition; noticing that you are enforcing one by hand is the handover to Module 11.
  • Module 9 has no runtime-sized sequence — that is Module 10, then Chapter 19.2.
  • Representation first, container second (Chapter 19.7).

Module 9 ends with a representation that is correct and incomplete. The Signal struct says what a signal is, and the thing that was supposed to hold a fileful of them is still a comment. Every type in the last eleven chapters has had its size fixed when the program was compiled, which is what made them cheap and what makes them unable to answer "however many are in the file." The single exception was std::string, and Chapter 9.5 measured exactly what it was doing: a 24-byte object whose characters moved from the stack region to the heap the first time they outgrew it. Something asked the system for that memory and something gave it back. The next module is that machinery in the open — what new and delete do, the three ways they are reliably got wrong, and the argument that no amount of care makes manual release survive real control flow. It is the module that makes the case for everything built on top of it.

Standards & specifications

Governing standard
ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)

Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C++ Programming curriculum.