重载类型转换运算符

在显式类型转换(强制转换)与 static_cast"中,你已了解到 C++ 允许将一种数据类型转换为另一种数据类型。以下示例将 int 转换为 double

int n{ 5 };
auto d{ static_cast<double>(n) }; // int 转换为 double

C++ 已内建各基本数据类型之间的转换规则,但默认情况下,它并不知道如何转换我们自定义的类类型。

在转换构造函数与 explicit 关键字中,我们展示了如何利用转换构造函数,从另一种类型的对象创建类类型对象。然而,这仅在目标类型为可修改的类类型时才可行。如果无法修改目标类型,该怎么办?

观察以下类:

class Cents
{
private:
    int m_cents{};
public:
    Cents(int cents = 0)
        : m_cents{ cents }
    {
    }

    int getCents() const { return m_cents; }
    void setCents(int cents) { m_cents = cents; }
};

该类非常简单:以整数保存若干分币,并提供访问/修改接口。它还包含一个将 int 转换为 Cents 的构造函数。

既然 int 可通过构造函数转换为 Cents,我们可能也希望提供 Cents 转 int 的功能。某些场景下这或许并不合适,但在此例中却合情合理。

作者注
一首小诗:

将整数化作分币身,
构造函数欣然应允。
然欲将分币返整数,
编译器却横眉拦阻。

为使转换得以成行,
我们须向编译器声明。
随后定义转换之径,
类型内容自此通行。

语法细节即将揭晓,
悬念不再把你缠绕。

一种不太理想的方式是使用转换函数。下例通过成员函数 getCents() “转换” Cents 变量为 int,以便用 printInt() 打印:

#include <iostream>

void printInt(int value)
{
    std::cout << value;
}

int main()
{
    Cents cents{ 7 };
    printInt(cents.getCents()); // 打印 7

    std::cout << '\n';

    return 0;
}

虽然能得到正确结果,但这并非真正的类型转换:编译器不会在强制转换或隐式转换时自动调用此函数。若频繁进行 Cents→int 转换,代码将充斥 getCents() 调用,十分杂乱。

还能怎么做?

重载类型转换运算符
此时便需重载类型转换运算符。该运算符可显式(通过强制转换)或隐式地被编译器用于类型转换。

以下示范如何重载类型转换运算符,使 Cents 可转为 int:

class Cents
{
private:
    int m_cents{};
public:
    Cents(int cents = 0)
        : m_cents{ cents }
    {
    }

    // 重载 int 转换
    operator int() const { return m_cents; }

    int getCents() const { return m_cents; }
    void setCents(int cents) { m_cents = cents; }
};

我们新增了一个名为 operator int() 的重载运算符。注意 operator 与目标类型之间有空格。

要点提示:

  • 重载类型转换运算符必须为非静态成员函数,并应加 const 修饰,以便用于 const 对象。
  • 无显式形参,因无法向其传递参数;但仍含隐式 *this 指针,指向待转换的对象。
  • 不声明返回类型。转换名(如 int)即返回类型,这是唯一允许的写法,避免冗余。

现在可以这样调用 printInt():

#include <iostream>

int main()
{
    Cents cents{ 7 };
    printInt(cents); // 打印 7

    std::cout << '\n';

    return 0;
}

编译器发现 printInt() 形参为 int,而实参 cents 不是 int,于是查找 Cents→int 的转换方式。找到后,调用 operator int() 返回 int,再传参给 printInt()。

也可显式使用 static_cast:

std::cout << static_cast<int>(cents);

你可以为任意数据类型提供重载类型转换运算符,包括自定义类型!

下面新定义 Dollars 类,提供到 Cents 的转换:

class Dollars
{
private:
    int m_dollars{};
public:
    Dollars(int dollars = 0)
        : m_dollars{ dollars }
    {
    }

    // 允许将 Dollars 转为 Cents
    operator Cents() const { return Cents{ m_dollars * 100 }; }
};

于是可直接将 Dollars 对象转为 Cents:

#include <iostream>

class Cents
{
private:
    int m_cents{};
public:
    Cents(int cents = 0)
        : m_cents{ cents }
    {
    }

    // 重载 int 转换
    operator int() const { return m_cents; }

    int getCents() const { return m_cents; }
    void setCents(int cents) { m_cents = cents; }
};

class Dollars
{
private:
    int m_dollars{};
public:
    Dollars(int dollars = 0)
        : m_dollars{ dollars }
    {
    }

    // 允许将 Dollars 转为 Cents
    operator Cents() const { return Cents{ m_dollars * 100 }; }
};

void printCents(Cents cents)
{
    std::cout << cents; // cents 将隐式转为 int
}

int main()
{
    Dollars dollars{ 9 };
    printCents(dollars); // dollars 隐式转为 Cents

    std::cout << '\n';

    return 0;
}

程序输出:

900

合情合理:9 美元即 900 分。

尽管可行,此处更推荐为 Dollars 添加转换构造函数(以 Cents 为参)。原因后述。

显式类型转换运算符

与 explicit 构造函数类似,也可将重载类型转换运算符声明为 explicit,禁止隐式转换。显式转换只能通过强制转换或直接初始化形式调用,不参与复制初始化。

#include <iostream>

class Cents
{
private:
    int m_cents{};
public:
    Cents(int cents = 0)
        : m_cents{ cents }
    {
    }

    explicit operator int() const { return m_cents; } // 标记为 explicit

    int getCents() const { return m_cents; }
    void setCents(int cents) { m_cents = cents; }
};

class Dollars
{
private:
    int m_dollars{};
public:
    Dollars(int dollars = 0)
        : m_dollars{ dollars }
    {
    }

    operator Cents() const { return Cents{ m_dollars * 100 }; }
};

void printCents(Cents cents)
{
    // std::cout << cents;                   // 不再可行,因 cents 不会隐式转 int
    std::cout << static_cast<int>(cents);  // 使用显式转换
}

int main()
{
    Dollars dollars{ 9 };
    printCents(dollars); // Dollars→Cents 隐式转换仍可行,因未标记 explicit

    std::cout << '\n';

    return 0;
}

通常应将类型转换运算符标记为 explicit,除非目标类型与源类型几乎同义。上文 Dollars::operator Cents() 未标记 explicit,因为 Dollars 可在任何需要 Cents 的上下文中使用,并无不妥。

最佳实践
与单参数转换构造函数类似,类型转换运算符应标记为 explicit,除非目标类型与源类型实质等同。

何时使用转换构造函数 vs 重载类型转换运算符

转换构造函数与重载类型转换运算符功能相似:

  • 转换构造函数:类 B 的成员函数,定义如何从 A 创建 B。
  • 重载类型转换运算符:类 A 的成员函数,定义如何将 A 转换为 B。

两者皆需定义成员函数,故仅可修改类类型时使用。若 A 不可修改,则无法用重载类型转换运算符;若 B 不可修改,则无法用转换构造函数;若两者均不可修改,则需非成员转换函数。

若 A、B 均可修改,理论上两者皆可,但应优先选择转换构造函数。让类类型自行负责构造,比依赖其他类更简洁。

最佳实践

需定义 A→B 的转换时:

  1. 若 B 为可修改的类类型,优先使用转换构造函数。
  2. 否则,若 A 为可修改的类类型,使用重载类型转换运算符。
  3. 否则,使用非成员函数进行转换。

当同时为同一转换定义重载类型转换运算符与转换构造函数时,二者均参与重载决议。视类型转换是否 const、对象是否 const、以及使用何种初始化(复制或直接),可能选中任一函数,或产生二义性(导致编译错误)。因此,应避免为同一转换同时定义两者。

最佳实践

定义 A→B 的转换方式时:

  • 若 B 为可修改类类型,优先用转换构造函数创建 B。
  • 否则,若 A 为可修改类类型,用重载类型转换运算符将 A 转换为 B。
  • 否则,使用非成员函数转换。

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"