10.1 — Introduction to program-defined (user-defined) types
What are user-defined / program-defined types?
Defining program-defined types
Program-defined type definitions always end in a semicolon.
Warning
Don’t forget to end your type definitions with a semicolon, otherwise the compiler will typically error on the next line of code.
Naming program-defined types
Best practice
Name your program-defined types starting with a capital letter and do not use a suffix.
Using program-defined types throughout a multi-file program
Best practice
A program-defined type used in only one code file should be defined in that code file as close to the first point of use as possible.
A program-defined type used in multiple code files should be defined in a header file with the same name as the program-defined type and then #included into each code file as needed.
Type definitions are partially exempt from the one-definition rule
There are two caveats that are worth knowing about. First, you can still only have one type definition per code file (this usually isn’t a problem since header guards will prevent this). Second, all of the type definitions for a given type must be identical, otherwise undefined behavior will result.
Nomenclature: user-defined types vs program-defined types
To provide additional differentiation, the C++20 language standard helpfully defines the term “program-defined type” to mean class types and enumerated types that are not defined as part of the standard library, implementation, or core language. In other words, “program-defined types” only include class types and enum types that are defined by us (or a third-party library).
Consequently, when talking only about class types and enum types that we’re defining for use in our own programs, we’ll prefer the term “program-defined”, as it has a more precise definition.
10.2 — Unscoped enumerations
Unscoped enumerations
Unscoped enumerations are defined via the enum
keyword.
// Define a new unscoped enumeration named Color
enum Color
{
// Here are the enumerators
// These symbolic constants define all the possible values this type can hold
// Each enumerator is separated by a comma, not a semicolon
red,
green,
blue, // trailing comma optional but recommended
}; // the enum definition must end with a semicolon
int main()
{
// Define a few variables of enumerated type Color
Color apple { red }; // my apple is red
Color shirt { green }; // my shirt is green
Color cup { blue }; // my cup is blue
Color socks { white }; // error: white is not an enumerator of Color
Color hat { 2 }; // error: 2 is not an enumerator of Color
return 0;
}
We start our example by using the enum keyword to tell the compiler that we are defining an unscoped enumeration, which we’ve named Color.
Inside a pair of curly braces, we define the enumerators for the Color type: red, green, and blue. These enumerators specify the set of possible values that objects of type Color will be able to hold. Each enumerator must be separated by a comma (not a semicolon) – a trailing comma after the last enumerator is optional but recommended for consistency.
A reminder
To quickly recap on nomenclature:
An enumeration or enumerated type is the program-defined type itself (e.g. Color)
An enumerator is a symbolic constant that is a possible value for a given enumeration (e.g. red)
Naming enumerations and enumerators
Best practice
Name your enumerated types starting with a capital letter. Name your enumerators starting with a lower case letter.
Enumerated types are distinct types
Putting enumerations to use
The scope of unscoped enumerations
Avoiding enumerator naming collisions
A better option is to put the enumerated type inside something that provides a separate scope region, such as a namespace:
namespace color
{
// The names Color, red, blue, and green are defined inside namespace color
enum Color
{
red,
green,
blue,
};
}
namespace feeling
{
enum Feeling
{
happy,
tired,
blue, // feeling::blue doesn't collide with color::blue
};
}
int main()
{
color::Color paint { color::blue };
feeling::Feeling me { feeling::blue };
return 0;
}
Best practice
Prefer putting your enumerations inside a named scope region (such as a namespace or class) so the enumerators don’t pollute the global namespace.
Alternatively, if an enumeration is only used within the body of a single function, the enumeration should be defined inside the function. This limits the scope of the enumeration and its enumerators to just that function. The enumerators of such an enumeration will shadow identically named enumerators defined in the global scope.
Comparing against enumerators
We can use the equality operators (operator== and operator!=) to test whether an enumeration has the value of a particular enumerator or not.
#include <iostream>
enum Color
{
red,
green,
blue,
};
int main()
{
Color shirt{ blue };
if (shirt == blue) // if the shirt is blue
std::cout << "Your shirt is blue!";
else
std::cout << "Your shirt is not blue!";
return 0;
}
10.3 — Unscoped enumeration input and output
Best practice
Avoid assigning explicit values to your enumerators unless you have a compelling reason to do so.
Unscoped enumerations will implicitly convert to integral values
Printing enumerator names
Teaching operator<< how to print an enumerator
Enumeration size and base
Best practice
Specify the base type of an enumeration only when necessary.
Warning
Because std::int8_t and std::uint8_t are usually type aliases for char types, using either of these types as the enum base will most likely cause the enumerators to print as char values rather than int values.
Integer to unscoped enumerator conversion
Unscoped enumerator input
10.4 — Scoped enumerations (enum classes)
Scoped enumerations
That solution is the scoped enumeration (often called an enum class in C++ for reasons that will become obvious shortly).
#include <iostream>
int main()
{
enum class Color // "enum class" defines this as a scoped enumeration rather than an unscoped enumeration
{
red, // red is considered part of Color's scope region
blue,
};
enum class Fruit
{
banana, // banana is considered part of Fruit's scope region
apple,
};
Color color { Color::red }; // note: red is not directly accessible, we have to use Color::red
Fruit fruit { Fruit::banana }; // note: banana is not directly accessible, we have to use Fruit::banana
if (color == fruit) // compile error: the compiler doesn't know how to compare different types Color and Fruit
std::cout << "color and fruit are equal\n";
else
std::cout << "color and fruit are not equal\n";
return 0;
}
This program produces a compile error on line 19, since the scoped enumeration won’t convert to any type that can be compared with another type.
As an aside…
The class keyword (along with the static keyword), is one of the most overloaded keywords in the C++ language, and can have different meanings depending on context. Although scoped enumerations use the class keyword, they aren’t considered to be a “class type” (which is reserved for structs, classes, and unions).
Scoped enumerations define their own scope regions
Unlike unscoped enumerations, which place their enumerators in the same scope as the enumeration itself, scoped enumerations place their enumerators only in the scope region of the enumeration. In other words, scoped enumerations act like a namespace for their enumerators. This built-in namespacing helps reduce global namespace pollution and the potential for name conflicts when scoped enumerations are used in the global scope.
To access a scoped enumerator, we do so just as if it was in a namespace having the same name as the scoped enumeration:
#include <iostream>
int main()
{
enum class Color // "enum class" defines this as a scoped enum rather than an unscoped enum
{
red, // red is considered part of Color's scope region
blue,
};
std::cout << red << '\n'; // compile error: red not defined in this scope region
std::cout << Color::red << '\n'; // compile error: std::cout doesn't know how to print this (will not implicitly convert to int)
Color color { Color::blue }; // okay
return 0;
}
Because scoped enumerations offer their own implicit namespacing for enumerators, there’s no need to put scoped enumerations inside another scope region (such as a namespace), unless there’s some other compelling reason to do so, as it would be redundant.
Scoped enumerations don’t implicitly convert to integers
Best practice
Favor scoped enumerations over unscoped enumerations unless there’s a compelling reason to do otherwise.
Easing the conversion of scoped enumerators to integers (advanced)
using enum
statements C++20
#include <iostream>
#include <string_view>
enum class Color
{
black,
red,
blue,
};
constexpr std::string_view getColor(Color color)
{
using enum Color; // bring all Color enumerators into current scope (C++20)
// We can now access the enumerators of Color without using a Color:: prefix
switch (color)
{
case black: return "black"; // note: black instead of Color::black
case red: return "red";
case blue: return "blue";
default: return "???";
}
}
int main()
{
Color shirt{ Color::blue };
std::cout << "Your shirt is " << getColor(shirt) << '\n';
return 0;
}
10.5 — Introduction to structs, members, and member selection
Defining structs
Because structs are a program-defined type, we first have to tell the compiler what our struct type looks like before we can begin using it. Here is an example of a struct definition for a simplified employee:
struct Employee
{
int id {};
int age {};
double wage {};
};
Tip
In common language, a member is a individual who belongs to a group. For example, you might be a member of the basketball team, and your sister might be a member of the choir.
In C++, a member is a variable, function, or type that belongs to a struct (or class). All members must be declared within the struct (or class) definition.
We’ll use the term member a lot in future lessons, so make sure you remember what it means.
Defining struct objects
10.6 — Struct aggregate initialization
What is an aggregate?
In general programming, an aggregate data type (also called an aggregate) is any type that can contain multiple data members.
In C++, the definition of an aggregate is narrower and quite a bit more complicated.
Putting the precise definition of a C++ aggregate aside, the important thing to understand at this point is that structs with only data members (which are the only kind of structs we’ll create in these lessons) are aggregates. Arrays (which we’ll cover next chapter) are also aggregates.
Aggregate initialization of a struct
Much like normal variables can be copy initialized, direct initialized, or list initialized, there are 3 forms of aggregate initialization:
struct Employee
{
int id {};
int age {};
double wage {};
};
int main()
{
Employee frank = { 1, 32, 60000.0 }; // copy-list initialization using braced list
Employee robert ( 3, 45, 62500.0 ); // direct initialization using parenthesized list (C++20)
Employee joe { 2, 28, 45000.0 }; // list initialization using braced list (preferred)
return 0;
}
Each of these initialization forms does a memberwise initialization, which means each member in the struct is initialized in the order of declaration. Thus, Employee joe { 2, 28, 45000.0 };
first initializes joe.id with value 2, then joe.age with value 28, and joe.wage with value 45000.0 last.
Best practice
Prefer the (non-copy) braced list form when initializing aggregates.
Missing initializers in an initializer list
If an aggregate is initialized but the number of initialization values is fewer than the number of members, then all remaining members will be value-initialized.
struct Employee
{
int id {};
int age {};
double wage {};
};
int main()
{
Employee joe { 2, 28 }; // joe.wage will be value-initialized to 0.0
return 0;
}
In the above example, joe.id will be initialized with value 2, joe.age will be initialized with value 28, and because joe.wage wasn’t given an explicit initializer, it will be value-initialized to 0.0.
This means we can use an empty initialization list to value-initialize all members of the struct:
Employee joe {}; // value-initialize all members
Const structs
Designated initializers C++20
struct Foo
{
int a{ };
int b{ };
int c{ };
};
int main()
{
Foo f1{ .a{ 1 }, .c{ 3 } }; // ok: f1.a = 1, f1.b = 0 (value initialized), f1.c = 3
Foo f2{ .b{ 2 }, .a{ 1 } }; // error: initialization order does not match order of declaration in struct
return 0;
}
Best practice
When adding a new member to an aggregate, it’s safest to add it to the bottom of the definition list so the initializers for other members don’t shift.
Assignment with an initializer list
struct Employee
{
int id {};
int age {};
double wage {};
};
int main()
{
Employee joe { 1, 32, 60000.0 };
joe = { joe.id, 33, 66000.0 }; // Joe had a birthday and got a raise
return 0;
}
Note that because we didn’t want to change joe.id, we needed to provide the current value for joe.id in our list as a placeholder, so that memberwise assignment could assign joe.id to joe.id. This is a bit ugly.
Assignment with designated initializers C++20
Designated initializers can also be used in a list assignment:
struct Employee
{
int id {};
int age {};
double wage {};
};
int main()
{
Employee joe { 1, 32, 60000.0 };
joe = { .id = joe.id, .age = 33, .wage = 66000.0 }; // Joe had a birthday and got a raise
return 0;
}
Any members that aren’t designated in such an assignment will be assigned the value that would be used for value initialization. If we hadn’t have specified a designated initializer for joe.id, joe.id would have been assigned the value 0.
10.7 — Default member initialization
When we define a struct (or class) type, we can provide a default initialization value for each member as part of the type definition. This process is called non-static member initialization, and the initialization value is called a default member initializer.
Key insight
Using default member initializers (or other mechanisms that we’ll cover later), structs and classes can self-initialize even when no explicit initializers are provided!
Explicit initialization values take precedence over default values
Missing initializers in an initializer list when default values exist
Recapping the initialization possibilities
The following example recaps all possibilities:
struct Something
{
int x; // no default initialization value (bad)
int y {}; // value-initialized by default
int z { 2 }; // explicit default value
};
int main()
{
Something s1; // No initializer list: s1.x is uninitialized, s1.y and s1.z use defaults
Something s2 { 5, 6, 7 }; // Explicit initializers: s2.x, s2.y, and s2.z use explicit values (no default values are used)
Something s3 {}; // Missing initializers: s3.x is value initialized, s3.y and s3.z use defaults
return 0;
}
The case we want to watch out for is s1.x. Because s1 has no initializer list and x has no default member initializer, s1.x remains uninitialized (which is bad, since we should always initialize our variables).
Always provide default values for your members
Best practice
Provide a default value for all members. This ensures that your members will be initialized even if the variable definition doesn’t include an initializer list.
Default initialization vs value initialization for aggregates
Fraction f1; // f1.numerator value initialized to 0, f1.denominator defaulted to 1
Fraction f2 {}; // f2.numerator value initialized to 0, f2.denominator defaulted to 1
value initialization: Fraction f2 {};
Preferring value initialization has one more benefit – it’s consistent with how we initialize objects of other types. Consistency helps prevent errors.
Best practice
If no explicit initializer values will be provided for an aggregate, prefer value initialization (with an empty braces initializer) to default initialization (with no braces).
10.8 — Struct passing and miscellany
Passing structs (by reference)
In the getZeroPoint() function above, we create a new named object (temp) just so we could return it:
Returning unnamed structs
Point3d getZeroPoint()
{
// We can create a variable and return the variable (we'll improve this below)
Point3d temp { 0.0, 0.0, 0.0 };
return temp;
}
The name of the object (temp) doesn’t really provide any documentation value here.
We can make our function slightly better by returning a temporary (unnamed) object instead:
Point3d getZeroPoint()
{
return Point3d { 0.0, 0.0, 0.0 }; // return an unnamed Point3d
}
In this case, a temporary Point3d is constructed, copied back to the caller, and then destroyed at the end of the expression. Note how much cleaner this is (one line vs two, and no need to understand whether temp is used more than once).
In the case where the function has an explicit return type (e.g. Point3d) instead of using type deduction (an auto return type), we can even omit the type in the return statement:
Point3d getZeroPoint()
{
// We already specified the type at the function declaration
// so we don't need to do so here again
return { 0.0, 0.0, 0.0 }; // return an unnamed Point3d
}
Also note that since in this case we’re returning all zero values, we can use empty braces to return a value-initialized Point3d:
Point3d getZeroPoint()
{
// We can use empty curly braces to value-initialize all members
return {};
}
Structs with program-defined members
Struct size and data structure alignment
Structs are an important building block
10.9 — Member selection with pointers and references
Member selection for structs and references to structs
In lesson 10.5 – Introduction to structs, members, and member selection, we showed that you can use the member selection operator (.) to select a member from a struct object:
#include
struct Employee
{
int id {};
int age {};
double wage {};
};
int main()
{
Employee joe { 1, 34, 65000.0 };
// Use member selection operator (.) to select a member from struct object
++joe.age; // Joe had a birthday
joe.wage = 68000.0; // Joe got a promotion
return 0;
}
Member selection for pointers to structs
Best practice
When using a pointer to access the value of a member, use the member selection from pointer operator (->) instead of the member selection operator (.).
Mixing pointers and non-pointers to members
10.10 — Class templates
Class templates
A reminder
A “class type” is a struct, class, or union type. Although we’ll be demonstrating “class templates” on structs for simplicity, everything here applies equally well to classes.
std::pair
Because working with pairs of data is common, the C++ standard library contains a class template named std::pair (in the header) that is defined identically to the Pair class template with multiple template types in the preceding section. In fact, we can swap out the pair struct we developed for std::pair:
#include <iostream>
#include <utility>
template <typename T, typename U>
void print(std::pair<T, U> p)
{
std::cout << '[' << p.first << ", " << p.second << ']';
}
int main()
{
std::pair<int, double> p1{ 1, 2.3 }; // a pair holding an int and a double
std::pair<double, int> p2{ 4.5, 6 }; // a pair holding a double and an int
std::pair<int, int> p3{ 7, 8 }; // a pair holding two ints
print(p2);
return 0;
}
We developed our own Pair class in this lesson to show how things work, but in real code, you should favor std::pair
over writing your own.
10.11 — Class template argument deduction (CTAD) and deduction guides
Author’s note
Many future lessons on this site make use of CTAD. If you’re compiling these examples using the C++14 standard, you’ll get an error about missing template arguments. You’ll need to explicitly add such arguments to the example to make it compile.
Template argument deduction guides C++17
Author’s note
A few notes about deduction guides.
First, std::pair
(and other standard library template types) come with pre-defined deduction guides. This is why our example above that uses std::pair
compiles fine in C++17 without us having to provide deduction guides ourselves.
Second, C++20 added the ability for the compiler to automatically generate deduction guides for aggregate class types, so the version of Pair
without the deduction guides should compile in C++20. This assumes your compiler supports feature P1816, which as of the time of writing, gcc and Visual Studio do, and Clang does not.
10.x — Chapter 10 summary and quiz
10.y — Using a language reference
A warning about the accuracy of cppreference
Cppreference is not an official documentation source – rather, it is a wiki. With wikis, anyone can add and modify content – the content is sourced from the community. Although this means that it’s easy for someone to add wrong information, that misinformation is typically quickly caught and removed, making cppreference a reliable source.
The only official source for C++ is the standard (Free drafts on github), which is a formal document and not easily usable as a reference.