• 深入理解C++20:类与对象的高级特性及运算符重载


    深入理解C++20:类与对象的高级特性及运算符重载

    类与对象的高级特性

    常量静态数据成员

    在你的类中,可以声明 const 数据成员,这意味着它们在创建和初始化后不能被改变。当常量仅适用于类时,应该使用 static const(或 const static)数据成员来代替全局常量,这也称为类常量。整型和枚举类型static const 数据成员即使不将它们作为内联变量,也可以在类定义内部定义和初始化。例如,你可能想要为Spreadsheet指定一个最大高度和宽度。如果用户尝试构造一个高度或宽度超过最大值的Spreadsheet,将使用最大值代替。你可以将最大高度和宽度作为 Spreadsheet 类的 static const 成员:

    export class Spreadsheet {
    public:
        // 省略简略性
        static const size_t MaxHeight { 100 };
        static const size_t MaxWidth { 100 };
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    你可以在构造函数中使用这些新常量,如下所示:

    Spreadsheet::Spreadsheet(size_t width, size_t height)
        : m_id { ms_counter++ },
          m_width { min(width, MaxWidth) } // std::min() 需要 
          m_height { min(height, MaxHeight) }
    {
        // 省略简略性
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    注意,你也可以选择在宽度或高度超过最大值时抛出异常,而不是自动将宽度和高度限制在其最大值内。但是,当你从构造函数中抛出异常时,析构函数将不会被调用,所以你需要小心处理这一点。这在第14章详细讨论了错误处理。

    数据成员的不同种类

    此类常量也可以用作参数的默认值。记住,你只能为从最右边参数开始的一连串参数提供默认值。这里有一个例子:

    export class Spreadsheet {
    public:
        Spreadsheet(size_t width = MaxWidth, size_t height = MaxHeight);
        // 省略简略性
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

    引用数据成员

    SpreadsheetSpreadsheetCells 很棒,但它们本身并不构成一个有用的应用程序。你需要代码来控制整个Spreadsheet程序,你可以将其打包到一个名为 SpreadsheetApplication 的类中。假设我们希望每个 Spreadsheet 都存储对应用程序对象的引用。SpreadsheetApplication 类的确切定义此刻并不重要,因此下面的代码简单地将其定义为一个空类。Spreadsheet 类被修改为包含一个新的引用数据成员,称为 m_theApp

    export class SpreadsheetApplication {
    };
    
    export class Spreadsheet {
    public:
        Spreadsheet(size_t width, size_t height, SpreadsheetApplication& theApp);
        // 省略简略性
    private:
        // 省略简略性
        SpreadsheetApplication& m_theApp;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这个定义为数据成员添加了一个 SpreadsheetApplication 引用。建议在这种情况下使用引用而不是指针,因为 Spreadsheet 应该总是引用一个 SpreadsheetApplication,而指针则不能保证这一点。**请注意,将应用程序的引用存储起来仅是为了演示引用作为数据成员的用法。**不建议以这种方式将 SpreadsheetSpreadsheetApplication 类耦合在一起,而是使用模型-视图-控制器(MVC)范例。

    在其构造函数中,应用程序引用被赋给每个 Spreadsheet。引用不能存在而不指向某些东西,因此 m_theApp 必须在构造函数的 ctor-initializer 中被赋值:

    Spreadsheet::Spreadsheet(size_t width, size_t height, SpreadsheetApplication& theApp)
        : m_id { ms_counter++ },
          m_width { std::min(width, MaxWidth) },
          m_height { std::min(height, MaxHeight) },
          m_theApp { theApp }
    {
        // 省略简略性
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    你还必须在拷贝构造函数中初始化引用成员。这是自动处理的,因为 Spreadsheet 拷贝构造函数委托给非拷贝构造函数,后者初始化了引用数据成员。记住,一旦你初始化了一个引用,你就不能改变它所引用的对象。在赋值操作符中不可能对引用进行赋值。根据你的用例,这可能意味着你的类不能为含有引用数据成员的类提供赋值操作符。如果是这种情况,赋值操作符通常被标记为删除。

    最后,引用数据成员也可以标记为 const。例如,你可能决定 Spreadsheets 只应该对应用程序对象有一个常量引用。你可以简单地更改类定义,将 m_theApp 声明为对常量的引用:

    export class Spreadsheet {
    public:
        Spreadsheet(size_t width, size_t height, const SpreadsheetApplication& theApp);
        // 省略简略性
    private:
        // 省略简略性
        const SpreadsheetApplication& m_theApp;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    嵌套类

    类定义不仅可以包含成员函数和数据成员,还可以编写嵌套类和结构体,声明类型别名或创建枚举类型。在类内部声明的任何内容都在该类的作用域内。如果它是公开的,你可以通过使用类名加上作用域解析运算符(ClassName::)来在类外部访问它。

    例如,你可能会决定 SpreadsheetCell 类实际上是 Spreadsheet 类的一部分。由于它成为 Spreadsheet 类的一部分,你可能会将其重命名为 Cell。你可以像这样定义它们:

    export class Spreadsheet {
    public:
        class Cell {
        public:
            Cell() = default;
            Cell(double initialValue);
            // 省略简略性
        };
        
        Spreadsheet(size_t width, size_t height, const SpreadsheetApplication& theApp);
        // 省略其他 Spreadsheet 声明
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    现在,Cell 类在 Spreadsheet 类内部定义,所以在 Spreadsheet 类外部引用 Cell 时,你必须使用 Spreadsheet:: 作用域来限定名称。这甚至适用于方法定义。例如,Cell 的双精度构造函数现在看起来像这样:

    Spreadsheet::Cell::Cell(double initialValue)
        : m_value { initialValue } {
    }
    
    • 1
    • 2
    • 3

    即使是在 Spreadsheet 类本身的方法的返回类型(但不是参数)中,也必须使用此语法:

    Spreadsheet::Cell& Spreadsheet::getCellAt(size_t x, size_t y) {
        verifyCoordinate(x, y);
        return m_cells[x][y];
    }
    
    • 1
    • 2
    • 3
    • 4

    直接在 Spreadsheet 类内部完全定义嵌套的 Cell 类会使 Spreadsheet 类的定义变得臃肿。你可以通过仅在 Spreadsheet 类中包含 Cell 的前向声明,然后分别定义 Cell 类来缓解这种情况:

    export class Spreadsheet {
    public:
        class Cell;
        Spreadsheet(size_t width, size_t height, const SpreadsheetApplication& theApp);
        // 省略其他 Spreadsheet 声明
    };
    
    class Spreadsheet::Cell {
    public:
        Cell() = default;
        Cell(double initialValue);
        // 省略简略性
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    普通的访问控制适用于嵌套类定义。如果你声明了一个私有或受保护的嵌套类,你只能从外部类内部使用它。嵌套类可以访问外部类的所有受保护和私有成员。而外部类只能访问嵌套类的公共成员。

    类内部的枚举类型

    枚举类型也可以是类的数据成员。例如,你可以添加对 SpreadsheetCell 类的单元格着色支持,如下所示:

    export class SpreadsheetCell {
    public:
        // 省略简略性
        
        enum class Color {
            Red = 1, Green, Blue, Yellow
        };
        
        void setColor(Color color);
        Color getColor() const;
        
    private:
        // 省略简略性
        Color m_color { Color::Red };
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    setColor()getColor() 方法的实现很直接:

    void SpreadsheetCell::setColor(Color color) {
        m_color = color;
    }
    
    SpreadsheetCell::Color SpreadsheetCell::getColor() const {
        return m_color;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    新方法的使用方式如下:

    SpreadsheetCell myCell { 5 };
    myCell.setColor(SpreadsheetCell::Color::Blue);
    auto color { myCell.getColor() };
    
    • 1
    • 2
    • 3

    运算符重载

    你经常需要对对象执行操作,例如添加它们、比较它们,或将它们流入流出文件。例如,Spreadsheet只有在你可以对其执行算术操作时才有用,比如求一整行单元格的和。

    重载比较运算符

    在你的类中定义比较运算符,如><<=>===!=,是非常有用的。C++20标准为这些运算符带来了很多变化,并增加了三元比较运算符,即太空船运算符<=>,在第1章中有介绍。为了更好地理解C++20所提供的内容,让我们先来看看在C++20之前你需要做些什么,以及在你的编译器还不支持三元比较运算符时你仍需要做些什么。

    就像基本的算术运算符一样,C++20之前的六个比较运算符应该是全局函数,这样你可以在运算符的左右两边的参数上使用隐式转换。比较运算符都返回一个布尔值。当然,你可以更改返回类型,但这并不推荐。这里是声明,你需要用==<>!=<=>=替换,从而产生六个函数:

    export class SpreadsheetCell { /* 省略以便简洁 */ };
    export bool operator<op>(const SpreadsheetCell& lhs, const SpreadsheetCell& rhs);
    
    • 1
    • 2

    以下是operator==的定义。其他的定义类似。

    bool operator==(const SpreadsheetCell& lhs, const SpreadsheetCell& rhs) {
        return (lhs.getValue() == rhs.getValue());
    }
    
    • 1
    • 2
    • 3

    注意:前述重载的比较运算符正在比较双精度值。大多数时候,对浮点值进行等于或不等于测试并不是一个好主意。你应该使用所谓的epsilon测试,但这超出了本书的范围。在具有更多数据成员的类中,比较每个数据成员可能很痛苦。然而,一旦你实现了==<,你就可以用这两个运算符来写其它的比较运算符。例如,这里是一个使用operator<operator>=定义:

    bool operator>=(const SpreadsheetCell& lhs, const SpreadsheetCell& rhs) {
        return !(lhs < rhs);
    }
    
    • 1
    • 2
    • 3

    你可以使用这些运算符来比较SpreadsheetCells与其他SpreadsheetCells,也可以与双精度和整型比较:

    if (myCell > aThirdCell || myCell < 10) {
        cout << myCell.getValue() << endl;
    }
    
    • 1
    • 2
    • 3

    **正如你所见,你需要编写六个不同的函数来支持六个比较运算符,这只是为了比较两个SpreadsheetCells。**随着当前六个实现的比较函数,可以将SpreadsheetCell与一个双精度值进行比较,因为双精度参数被隐式转换为SpreadsheetCell。如前所述,这种隐式转换可能效率低下,因为需要创建临时对象。就像之前的operator+一样,你可以通过实现显式函数来避免与双精度的比较。对于每个运算符,你将需要以下三个重载:

    bool operator<op>(const SpreadsheetCell& lhs, const SpreadsheetCell& rhs);
    bool operator<op>(double lhs, const SpreadsheetCell& rhs);
    bool operator<op>(const SpreadsheetCell& lhs, double rhs);
    
    • 1
    • 2
    • 3

    如果你想支持所有比较运算符,那么需要编写很多重复的代码!

    C++20

    现在让我们转换一下思路,看看C++20带来了什么。C++20极大地简化了为你的类添加比较运算符的支持。首先,使用C++20,实际上建议将operator==实现为类的成员函数,而不是全局函数。还要注意,添加[[nodiscard]]属性是个好主意,这样运算符的结果就不能被忽略了。这里是一个例子:

    [[nodiscard]] bool operator==(const SpreadsheetCell& rhs) const;
    
    • 1

    使用C++20,这一个operator==重载就可以使以下比较工作:

    if (myCell == 10) {
        cout << "myCell == 10\n";
    }
    if (10 == myCell) {
        cout << "10 == myCell\n";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    例如10==myCell这样的表达式会被C++20编译器重写为myCell==10,可以调用operator==成员函数。此外,通过实现operator==,C++20会自动增加对!=的支持。

    接下来,为了实现对完整套比较运算符的支持,在C++20中你只需要实现一个额外的重载运算符,即operator<=>。一旦你的类有了operator==<=>的重载,C++20会自动为所有六个比较运算符提供支持!对于SpreadsheetCell类,operator<=>如下所示:

    [[nodiscard]] std::partial_ordering operator<=>(const SpreadsheetCell& rhs) const;
    
    • 1

    注意:C++20编译器不会用<=>重写==!=比较,这是为了避免性能问题,因为显式实现operator==通常比使用<=>更高效。

    SpreadsheetCell中存储的值是一个双精度值。请记住,从第1章开始,浮点类型只有部分排序,这就是为什么重载返回std::partial_ordering。实现很简单:

    std::partial_ordering SpreadsheetCell::operator<=>(const SpreadsheetCell& rhs) const {
        return getValue() <=> rhs.getValue();
    }
    
    • 1
    • 2
    • 3

    通过实现operator<=>,C++20会自动为>、`<

    <=>=提供支持,通过将使用这些运算符的表达式重写为使用<=>的表达式。例如,类似于myCell的表达式会自动重写为类似于std::is_ lt(myCell<=>aThirdCell)的东西,其中is_lt()是一个命名比较函数;请参见第1章。所以,通过只实现operator==operator<=>`,SpreadsheetCell类支持完整的比较运算符集:

    if (myCell < aThirdCell) {
        // ...
    }
    if (aThirdCell < myCell) {
        // ...
    }
    if (myCell <= aThirdCell) {
        // ...
    }
    if (aThirdCell <= myCell) {
        // ...
    }
    if (myCell > aThirdCell) {
        // ...
    }
    if (aThirdCell > myCell) {
        // ...
    }
    if (myCell >= aThirdCell) {
        // ...
    }
    if (aThirdCell >= myCell) {
        // ...
    }
    if (myCell == aThirdCell) {
        // ...
    }
    if (aThirdCell == myCell) {
        // ...
    }
    if (myCell != aThirdCell) {
        // ...
    }
    if (aThirdCell != myCell) {
        // ...
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    由于SpreadsheetCell类支持从双精度到SpreadsheetCell的隐式转换,因此也支持以下比较:

    if (myCell < 10) {
    }
    if (10 < myCell) {
    }
    if (10 != myCell) {
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    就像比较两个SpreadsheetCell对象一样,编译器会将这些表达式重写为使用operator==<=>的形式,并根据需要交换参数的顺序。例如,10首先被重写为类似于is_lt(10<=>myCell)的东西,这不会起作用,因为我们只有<=>作为成员的重载,这意味着左侧参数必须是SpreadsheetCell。注意到这一点后,编译器再尝试将表达式重写为类似于is_gt(myCell<=>10)的东西,这就可以工作了。与以前一样,如果你想避免隐式转换的轻微性能影响,你可以为双精度提供特定的重载。而这现在,多亏了C++20,甚至不是很多工作。你只需要提供以下两个额外的重载运算符作为方法:

    [[nodiscard]] bool operator==(double rhs) const;
    [[nodiscard]] std::partial_ordering operator<=>(double rhs) const;
    
    • 1
    • 2

    这些实现如下:

    bool SpreadsheetCell::operator==(double rhs) const {
        return getValue() == rhs;
    }
    std::partial_ordering SpreadsheetCell::operator<=>(double rhs) const {
        return getValue() <=> rhs;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    编译器生成的比较运算符

    在查看SpreadsheetCelloperator==<=>的实现时,可以看到它们只是简单地比较所有数据成员。在这种情况下,我们可以进一步减少编写代码的行数,因为C++20可以为我们完成这些工作。就像可以显式默认化拷贝构造函数一样,operator==<=>也可以被默认化,这种情况下编译器将为你编写它们,并通过依次比较每个数据成员来实现它们。此外,如果你只显式默认化operator<=>,编译器还会自动包含一个默认的operator==。因此,对于没有显式operator==<=>用于双精度的SpreadsheetCell版本,我们可以简单地编写以下单行代码,为比较两个SpreadsheetCell添加对所有六个比较运算符的完全支持:

    [[nodiscard]] std::partial_ordering operator<=>(const SpreadsheetCell&) const = default;
    
    • 1

    此外,你可以将operator<=>的返回类型使用auto,这种情况下编译器会基于数据成员的<=>运算符的返回类型来推断返回类型。如果你的类有不支持operator<=>的数据成员,那么返回类型推断将不起作用,你需要显式指定返回类型为strong_orderingpartial_orderingweak_ordering。为了让编译器能够编写默认的<=>运算符,类的所有数据成员都需要支持operator<=>,这种情况下返回类型可以是auto,或者是operator<==,这种情况下返回类型不能是auto。由于SpreadsheetCell有一个双精度数据成员,编译器推断返回类型为partial_ordering

    [[nodiscard]] auto operator<=>(const SpreadsheetCell&) const = default;
    
    • 1

    单独的显式默认化的operator<=>适用于没有显式operator==<=>用于双精度的SpreadsheetCell版本。如果你添加了这些显式的双精度版本,你就添加了一个用户声明的operator==(double)。因为这个原因,编译器将不再自动生成operator==(const SpreadsheetCell&),所以你必须自己显式默认化一个,如下所示:

    export class SpreadsheetCell {
    public:
        // Omitted for brevity
        [[nodiscard]] auto operator<=>(const SpreadsheetCell&) const = default;
        [[nodiscard]] bool operator==(const SpreadsheetCell&) const = default;
        [[nodiscard]] bool operator==(double rhs) const;
        [[nodiscard]] std::partial_ordering operator<=>(double rhs) const;
        // Omitted for brevity
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    如果你的类可以显式默认化operator<=>,我建议这样做,而不是自己实现它。通过让编译器为你编写,它将随着新添加或修改的数据成员保持最新状态。如果你自己实现了运算符,那么每当你添加数据成员或更改现有数据成员时,你都需要记得更新你的operator<=>实现。如果operator==没有被编译器自动生成,同样的规则也适用于它。只有当它们作为参数有对类类型的引用时,才能显式默认化operator==<=>。例如,以下不起作用:

    [[nodiscard]] auto operator<=>(double) const = default; // 不起作用!
    
    • 1

    注意:要在C++20中向类添加对所有六个比较运算符的支持:
    ➤ 如果默认化的operator<=>适用于你的类,那么只需要一行代码显式默认化operator<=>作为方法即可。在某些情况下,你可能需要显式默认化operator==
    ➤ 否则,只需重载并实现operator==<=>作为方法。无需手动实现其他比较运算符。

  • 相关阅读:
    【NLP教程】用python调用百度AI开放平台进行情感倾向分析
    Spring Boot配置多个Kafka数据源
    彻底解决JDK安装包点击后无反应
    步道乐跑位置模拟
    4D毫米波雷达加速向上,搭载福瑞泰克解决方案量产车型预计年底上市
    ​Unity Vuforia 新手(图片识别)教程,后续整理 实体识别 详细流程
    接口测试经验分享
    备战面试每日一题
    java-net-php-python-ssm房车买卖租赁专用网站计算机毕业设计程序
    12、设计模式之代理模式(Proxy)
  • 原文地址:https://blog.csdn.net/qq_42896106/article/details/134353584