在显式类型转换(强制转换)与 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 的转换时:
- 若 B 为可修改的类类型,优先使用转换构造函数。
- 否则,若 A 为可修改的类类型,使用重载类型转换运算符。
- 否则,使用非成员函数进行转换。
当同时为同一转换定义重载类型转换运算符与转换构造函数时,二者均参与重载决议。视类型转换是否 const、对象是否 const、以及使用何种初始化(复制或直接),可能选中任一函数,或产生二义性(导致编译错误)。因此,应避免为同一转换同时定义两者。
最佳实践
定义 A→B 的转换方式时:
- 若 B 为可修改类类型,优先用转换构造函数创建 B。
- 否则,若 A 为可修改类类型,用重载类型转换运算符将 A 转换为 B。
- 否则,使用非成员函数转换。