What Are Program-Defined Types?

A program-defined type is a type you create yourself, such as an enumeration or a struct, to model something the built-in types cannot express on their own.

The need shows up quickly. A measurement is a value together with a unit, and those two facts belong together: separating them into two variables means nothing stops you from pairing the wrong ones, and every function dealing with measurements needs two parameters instead of one. C++ has no built-in measurement type, and it never will, because no language can anticipate the hundreds of concepts programs need. What it offers instead is the ability to add your own.

Why Some Types Need Defining and Others Do Not

Fundamental types are part of the core language, so the compiler already knows them:

int count;
double temperature;

The same goes for compound types built directly out of other types, such as functions, pointers, references, and arrays. Writing int* address; requires no preparation, because the language already assigns meaning to the *.

Introducing a new name is different. A type alias creates an identifier the compiler has never seen, so it has to be defined before use:

#include <iostream>

using Distance = int;

int main()
{
    Distance miles{ 100 };

    std::cout << miles << '\n';

    return 0;
}

Remove the alias and the compiler has no idea what a Distance is. Note what the definition does and does not do: it creates no object and allocates nothing, it only explains the name.

Program-defined types work the same way, and for the same reason.

The Two Categories

C++ gives you two kinds of compound type from which to build program-defined types:

  • Enumerated types, both unscoped and scoped enumerations
  • Class types, meaning structs, classes, and unions

Both require a name and a definition before use, which is the property that sets them apart from the other compound types.

Key Concept
A program-defined type needs both a name and a definition before it can be used. The other compound types need neither.

Functions do not count as user-defined types, even though they also need a name and a definition. What gets named there is the function, not its type. Those are called user-defined functions instead.

Defining One

Structs are covered properly later in this chapter, but the shape is worth seeing now:

#include <iostream>

struct Measurement
{
    int value{};
    int unit{};
};

int main()
{
    Measurement distance{ 50, 1 };

    std::cout << "Value: " << distance.value << ", unit: " << distance.unit << '\n';

    return 0;
}

Output:

Value: 50, unit: 1

The struct keyword defines a new type named Measurement at global scope, so the rest of the file can use it. That definition allocates nothing; it describes what a Measurement looks like. The line inside main is what actually creates one.

Warning
A type definition must end with a semicolon.

That warning earns its place because of how the mistake presents itself. Leave the semicolon off and GCC reports:

s.cpp:7:2: error: expected ';' after struct definition
    7 | }
      |  ^
      |  ;

This compiler names the problem precisely, but the error points at the closing brace, and other compilers report it further down at whatever followed the definition. An error on a line that looks perfectly correct is usually this.

Naming Them

By convention, a program-defined type starts with a capital letter and carries no suffix: Measurement, not measurement, measurement_t, or Measurement_t.

Best Practice
Start program-defined type names with a capital letter, and do not add a suffix.

That convention produces declarations like this one, which reads oddly at first:

Measurement measurement{};

It follows the same pattern as every other definition: type first, then variable name, then an optional initializer. The capital letter tells you which is which, and since C++ is case-sensitive there is no conflict.

Using a Type Across Multiple Files

Every file that uses a program-defined type must see the complete definition. A forward declaration will not do, because the compiler has to know how much memory an object of the type occupies.

The mechanism is the usual one: put the definition in a header named after the type, and include it wherever it is needed.

Measurement.h:

#pragma once

struct Measurement
{
    int value{};
    int unit{};
};

Measurement.cpp:

#include "Measurement.h"

#include <iostream>

int main()
{
    Measurement distance{ 50, 1 };

    std::cout << distance.value << '\n';

    return 0;
}
Best Practice
Define a type used in only one file inside that file, as close to its first use as you can. Define a type used in several files in a header named after the type, and include it where needed.

The Partial ODR Exemption

The one-definition rule says each function and global variable gets one definition per program, with forward declarations covering the other files. That arrangement cannot work for types, since a declaration does not tell the compiler enough to use the type at all.

So types are partially exempt: the same type may be defined in many code files. You have relied on this already. Two files that both include <iostream> each receive the full set of definitions from that header, and the program links fine.

Two limits apply. One definition per code file, which header guards or #pragma once handle for you. And every definition of a given type must be identical across the program, or the behavior is undefined.

User-Defined Versus Program-Defined

The two terms are often used interchangeably in conversation, but the standard draws a line:

Category Meaning Examples
Fundamental Built into the core language int, std::nullptr_t
Compound Defined in terms of other types int&, double*, std::string, Measurement
User-defined Any class type or enumerated type, including those from the standard library or the implementation std::string, Measurement
Program-defined Class and enumerated types excluding those from the standard library, the implementation, and the core language Measurement

The consequence that surprises people is that std::string is a user-defined type by the standard's definition, since it is a class type someone wrote rather than a core language type. C++20 introduced "program-defined type" precisely to name the narrower idea, which is what you usually mean, so that is the term this course uses for types you write yourself.

Summary

  • Program-defined types are types you create, in two flavors: enumerated types and class types, which covers structs, classes, and unions
  • Fundamental and compound types are usable immediately, while a program-defined type needs a name and a full definition before use
  • A type definition ends with a semicolon, and forgetting it produces an error pointing at the closing brace or the line after the definition
  • Name types with a capital letter and no suffix, so Measurement holds a measurement
  • Every file using the type needs the complete definition, so types shared between files live in a header named after the type
  • Types are partially exempt from the ODR: many files may define the same type, but each file only once, and every definition must be identical
  • The standard's user-defined type includes standard library types such as std::string, while program-defined type covers only the ones you write