A Tour of C++ 2.1–2.2|user-defined type과 struct Vector

Answer First
Built-in types are low-level tools that mirror hardware capabilities. C++ layers abstraction mechanisms on top to let programmers design and use their own types — user-defined types. The first step in creating a new type is grouping the necessary pieces with a struct. The Tour’s introductory example, Vector, starts with just two members — elem and sz — initialized by a standalone vector_init function.


How do built-in types differ from user-defined types?

Stroustrup defines built-in types as the set of types constructible from:

  • Fundamental types (§1.4): int, double, char, bool, etc.
  • The const modifier (§1.6)
  • Declarator operators (§1.7): * (pointer), & (reference), [] (array)

By this definition, const int*, int&, and double[10] are all built-in types — they are combinations of fundamental types with modifiers and declarators, not newly named types.

Why are built-ins deliberately low-level?

The book states that C++‘s built-in types and operations are intentionally low-level. They directly and efficiently reflect the capabilities of ordinary computer hardware. That is a strength for performance and predictability, but it means they do not by themselves provide the high-level facilities that application programmers need.

For example, a CPU knows how to load an int into a register. But “treat temperature, humidity, and a timestamp as a single logical unit” is not a hardware concept. Bridging that gap is exactly what C++‘s abstraction mechanisms are for.

Two goals of C++ abstraction mechanisms

C++ adds abstraction mechanisms on top of built-in types. These mechanisms serve two purposes:

  1. They allow programmers to design and implement their own types with appropriate representations and operations.
  2. They enable those types to be used simply and elegantly.

Types assembled via these mechanisms are called user-defined types. The book introduces them as classes and enumerations (with unions appearing at §2.5).

Standard library types are also user-defined

A common misconception is that “user-defined” means “a type I wrote in today’s source file.” In fact, std::string, std::vector<int>, and std::optional<T> are all user-defined types — they are assembled by the C++ type system, not hard-wired into the language for specific hardware architectures.

ExampleClassification
intbuilt-in (fundamental type)
const int*built-in (int + const + *)
double[10]built-in (double + [])
std::stringuser-defined (class-based)
std::vector<int>user-defined (class template)

Why prefer user-defined types?

The book gives three reasons to choose user-defined types over raw built-in combinations:

  1. Easier to use. A type with a meaningful name and operations communicates intent clearly at call sites.
  2. Fewer mistakes. The compiler enforces type rules, catching misuse early.
  3. Typically as efficient — or even more efficient — than hand-rolled built-in code. Manually managing low-level types invites boundary-check omissions, lifetime errors, and cache-unfriendly layouts. A well-designed type prevents those mistakes, and inlining plus move semantics often produce machine code on par with or better than hand-written equivalents.

A concrete example: Sample struct vs. loose parameters

Study notes offer a practical illustration. Handling sensor data with only built-in types:

void log(double t, double h, int ts);
log(21.5, 40.0, 1710000000);

If arguments are swapped, the compiler is silent — there is nothing in the signature that encodes which double is temperature and which is humidity.

Group them into a type:

struct Sample {
    double temperature;
    double humidity;
    int    timestamp;
};

void log(const Sample& s);

Now the call site knows only Sample. If temperature changes from double to float, or a new field is added, the change is localized to the Sample definition. The same reasoning applies everywhere: coordinates become Point instead of double x, y; monetary values become Money instead of long + string; buffers become std::vector<std::byte> instead of char* + int.

Don’t Panic — where Ch.2 fits

Borrowing from Douglas Adams, Stroustrup’s motto for Ch.2 is “Don’t Panic!” There is no need to implement a full std::vector-quality type right now. Mechanisms are introduced one at a time:

  • Ch.2 — Minimal mechanisms: struct, class, enum, union
  • Ch.4–8 — Abstraction proper: error handling, class hierarchies, operators, templates
  • Ch.9–17 — Standard library: examples of what those mechanisms can produce

If Ch.1 supplies the LEGO bricks, Ch.2 begins teaching how to design components from those bricks.


What does struct Vector hold?

What a struct aggregates

What a struct aggregates

The first step in building a user-defined type is gathering the necessary pieces into a struct. The Tour’s teaching example is:

struct Vector {
    double* elem;  // pointer to elements
    int sz;        // number of elements
};

Vector consists of a double* pointer (elem) pointing to a dynamically allocated array of doubles, and an int (sz) recording the number of elements.

A bare declaration is almost useless

Vector v;  // box created; elem points nowhere

This declaration carves out stack space for a Vector object but leaves elem uninitialized. Accessing v.elem[0] at this point triggers undefined behavior.

Capitalized Vector vs. lowercase vector

The book uses capitalized Vector to distinguish its teaching type from the standard library’s lowercase std::vector. Do not reimplement std::vector or std::string — use them. The Vector in these sections exists solely to demonstrate language mechanics.


Why are & and new necessary in vector_init?

After declaring a Vector, its members must be initialized. The Tour introduces a standalone initialization function:

void vector_init(Vector& v, int s)
{
    v.elem = new double[s];  // allocate an array of s doubles
    v.sz = s;
}

Why the non-const reference &?

Vector& v is a non-const reference (§1.7). It gives vector_init a direct alias to the caller’s object, not a copy.

Without &:

void vector_init(Vector v, int s)  // receives a copy
{
    v.elem = new double[s];  // initializes the copy's elem
    v.sz = s;
}                            // copy is destroyed; caller's Vector unchanged

The caller’s Vector would never be initialized. Its elem would remain a dangling, uninitialized pointer.

How new uses the free store

new double[s] allocates memory on the free store (dynamic memory; also called the heap). Objects allocated there:

  • Are independent of the scope that allocated them.
  • Remain alive until explicitly released with delete (§5.2.2).

The current Vector has no delete code. This is an intentional pedagogical omission — the destructor and proper resource management are introduced in §5.2.2.

new vs. malloc — brief comparison

mallocnew
Unitraw bytestyped object
Initializationnone (no constructor)constructor called
Releasefreedelete / delete[]
Return typevoid* (cast required)T*

new double[s] allocates s doubles and returns double*, which matches elem. By contrast, new Vector allocates a single Vector box and returns Vector* — a type mismatch for elem. Never mix new with free or malloc with delete.

Memory layout after vector_init(v, 3)

After vector_init(v, 3)

v (stack)
┌─────────────┬──────┐
│ elem ───────┼──┐   │  sz = 3
└─────────────┴──┼───┘

          (free store)
          [ ? | ? | ? ]
           [0]  [1]  [2]

The read_and_sum example

double read_and_sum(int s)
    // read s integers from cin and return their sum; s is assumed to be positive
{
    Vector v;
    vector_init(v, s);

    for (int i = 0; i != s; ++i)
        cin >> v.elem[i];

    double sum = 0;
    for (int i = 0; i != s; ++i)
        sum += v.elem[i];
    return sum;
}

The caller must know elem and sz — the representation is fully exposed. This is the limitation §2.3 addresses with constructors and encapsulation.


How do . and -> member access differ?

. vs -> member access

. vs -> member access

The appropriate member-access operator depends on what kind of variable you have:

void f(Vector v, Vector& rv, Vector* pv)
{
    int i1 = v.sz;    // v is an object — use dot (.)
    int i2 = rv.sz;   // rv is a reference — use dot (.)
    int i3 = pv->sz;  // pv is a pointer — use arrow (->)
}
Variable kindOperatorExample
Object (named).v.sz
Reference.rv.sz
Pointer->pv->sz

pv->sz is strictly equivalent to (*pv).sz — dereference the pointer, then access the member. Writing pv.sz on a pointer variable causes a compile error because Vector* has no member named sz.


FAQ

Q: Is const int* a built-in or a user-defined type?

A: Built-in. It is formed from the fundamental type int combined with the const modifier and the pointer declarator *. No new named type is defined, so it does not qualify as user-defined.

Q: Is std::string a built-in type?

A: No. Despite being part of the standard library, std::string is a user-defined type assembled via the class mechanism. “User-defined” does not mean “a type the end programmer wrote” — it means “assembled through the C++ type system rather than hard-wired into the language for a specific CPU.”

Q: What happens if & is omitted from vector_init?

A: The function receives a copy of the caller’s Vector. The new double[s] allocation is stored in the copy’s elem. When the function returns, the copy is destroyed, and the caller’s original Vector remains with an uninitialized elem. The allocated memory is also leaked.

Q: Why is v.elem[0] = 1.0; dangerous immediately after Vector v;?

A: Because elem has not been set to point at a valid array. Dereferencing an uninitialized pointer is undefined behavior — it may crash, silently corrupt memory, or do anything else the implementation allows.

Q: Should I reimplement std::vector to learn from it?

A: No. Use std::vector and std::string directly. The Capitalized Vector in the Tour is a teaching vehicle to illustrate language mechanisms — it is intentionally incomplete (no destructor, no bounds checking). For production code, rely on the decades-tested standard library.


Sources


Up next — A Tour of C++ Ch.2 part 2/2: Classes
In §2.3, a constructor replaces the manual vector_init call, and the representation is hidden behind an interface — the first step toward proper encapsulation. Full abstraction mechanisms (class hierarchies, destructors, templates) are covered in Ch.4–8.


Disclaimer: This article is a study summary based on Bjarne Stroustrup, A Tour of C++ (3rd ed.) §§2.1–2.2 and personal study notes. Behavior may vary by standard version, implementation, and compiler. Do not reimplement std::vector or std::string — use the standard library instead.