Compound Types: Enums and Structs___CH_10

本文介绍了C++中用户自定义类型(如类和枚举)的定义、命名规范以及使用方法。详细讲解了无作用域枚举和有作用域枚举的差异,包括它们的初始化、输入输出以及在多文件程序中的使用。此外,还探讨了结构体的默认成员初始化和传递方式,以及类模板和模板参数推导(CTAD)的概念。
摘要由CSDN通过智能技术生成

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.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值