Item 24:若所有参数都需要类型转换,请采用非成员函数
对齐 Effective C++ 第三版 Item 24:用 Rational operator* 解释成员函数对左右操作数的隐式转换不对称,改用非成员函数让所有参数参与转换,并约束安全转换、ADL 与模板推导边界。
学习目标
- 能复现 member operator* 中
r * 2成功而2 \* r失败的候选与转换过程 - 能实现 namespace non-member function,使左右 operands 按相同规则参与安全 implicit conversion
- 能判断 converting constructor、explicit、hidden friend 与 function template 的适用边界
从左右互换后编译失败开始
有理数允许整数构造:
class Rational {
public:
Rational(int numerator = 0, int denominator = 1);
Rational operator*(const Rational& rhs) const;
};调用结果却不对称:
Rational oneHalf(1, 2);
Rational a = oneHalf * 2; // 成功
Rational b = 2 * oneHalf; // 失败数学乘法的 operands 地位相同,但 member function 把左 operand 隐藏为 this。这改变了候选函数建立与参数转换的规则。
Item 24 的原则是 Declare non-member functions when type conversions should apply to all parameters(若所有参数都需要类型转换,请采用非成员函数)。
为什么右参数能够转换
oneHalf * 2 等价于:
oneHalf.operator*(2);receiver 已经是 Rational,member candidate 明确存在。编译器检查普通参数 rhs 时,可用 Rational(2) 把 int 转成 Rational。
oneHalf.operator*( Rational(2) )转换后的右参数与 const Rational& 匹配,调用成立。
为什么左侧不能先转换
2 * oneHalf 若使用 member 设计,需要先把 2 变成 Rational,再调用其 member。但候选查找从左 operand 的类型开始;int 没有 Rational 的 member operator*。
编译器不会为了发现一个尚不存在的 member candidate,先猜测把 receiver 转成任意用户类型。显式写 Rational(2) * oneHalf 才能成立。
r.operator*(2)receiver 已是 Rational,右参数可由 int 隐式转换。
2.operator*(r)左侧 int 没有该 member,候选建立前不能先变 receiver。
member exact显式构造右参数后两侧类型匹配。
member exact显式构造左 receiver 后调用成立。
lhs != rhs rules数学对称操作因 this 隐藏参数而具有不同转换待遇。
free operator*(lhs,rhs)两侧都成为普通参数,使用同一候选转换规则。
非成员函数让两侧地位相同
把乘法移出 class:
namespace math {
class Rational {
public:
Rational(int numerator = 0, int denominator = 1);
int numerator() const noexcept;
int denominator() const noexcept;
};
Rational operator*(const Rational& lhs, const Rational& rhs) {
return Rational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}
} // namespace math现在 oneHalf * 2 可把 rhs 转为 Rational,2 * oneHalf 可把 lhs 转为 Rational。type conversions apply to all parameters(所有参数参与类型转换)。
2 * r构造二元 operator 表达式并收集可见候选。
namespace rationalRational 参数使其 namespace free operator 进入集合。
const Rational&r 精确匹配右参数。
2 -> Rational{2}非 explicit converting constructor 提供用户定义转换。
operator*(Rational,Rational)两个普通参数绑定后执行领域乘法。
Rational value结果按值返回,不泄漏 operand 或 ownership。
ADL 帮助发现同命名空间操作符
对 2 * oneHalf,普通查找与 argument-dependent lookup 会考虑 Rational 所在 namespace 的 non-member operator。
只要一个 operand 已是 Rational,ADL 可找到同 namespace operator;另一个 operand 再接受安全转换。若两个 operands 都是 int,则没有 Rational associated namespace,也不应把普通整数乘法劫持成 Rational。
↡由实参类型确定、供 ADL 搜索函数声明的 namespace 与 class 集合。函数应与类型放同 namespace,而不是散落 global scope 或添加到 std。
通常不需要 friend
Item 24 要求 non-member,不等于要求 friend。若 public accessors 足以实现乘法,应保持 non-member non-friend,减少 private representation 依赖。
↡非成员函数仅使用 public contract,不获得读取私有表示的额外权限。如果 performance/invariant 真正要求直接访问,可考虑 narrow friend;但需要把它计入封装边界。不能因为 operator 写在 class 附近就自动 friend。
↡在无法通过稳定 public API 正确高效实现时,向特定 non-member 授予的最小私有权限。只有安全转换才应隐式发生
non-member 解决的是“两侧能否同等参与转换”,不证明转换本身合理。int 到 Rational 通常无损且含义唯一:整数 n 对应 n/1。
↡不丢失信息、没有单位歧义且保持领域不变量的自动类型转换。Money、Angle、Duration 等需要单位或上下文,裸 int 隐式转换可能危险:
class Money {
public:
explicit Money(std::int64_t minorUnits, Currency currency);
};如果转换会 narrowing、分配、失败或依赖 global context,应使用 explicit、named factory 或 Result,而不是为了 operator 对称性放宽契约。
修改型操作仍适合成员
operator* 产生独立值且 operands 对称;operator*= 修改左对象,lhs 有明确 identity,因此通常是 member。
class Rational {
public:
Rational& operator*=(const Rational& rhs);
};
Rational operator*(Rational lhs, const Rational& rhs) {
lhs *= rhs;
return lhs;
}这种分层把 invariant-critical mutation 留给 member,同时让 free operator 提供对称接口和转换。
explicit constructor 时提供显式 overload
若领域要求 constructor explicit,但仍希望支持 Rational * int,可以提供明确 overload,而不是打开所有 int 转换:
Rational operator*(const Rational& lhs, int rhs);
Rational operator*(int lhs, const Rational& rhs);overload 可做范围检查并定义返回类型。代价是组合数量增加,需要避免 ambiguity 和重复实现。
↡多个 overload 对同一表达式具有同等 conversion rank,编译器无法选择唯一候选。优先从领域 API 需要的组合出发,不要生成所有数值类型笛卡尔积。
hidden friend 是现代查找工具但仍是 friend
operator 可在 class 内定义为 friend,成为 non-member 并主要通过 ADL 找到:
class Rational {
friend Rational operator*(Rational lhs, const Rational& rhs) {
lhs *= rhs;
return lhs;
}
};它对防止无关隐式转换和模板 generic operators 很有用,但拥有 private access。若不需要权限,namespace non-friend 更清楚地维持封装;若采用 hidden friend,应明确它属于 joint representation boundary。
↡class 与获权 operator 必须共同审查 representation 变化的维护范围。function template 的推导规则不同
将 operator 泛化为 function template 时,template argument deduction 不会靠 user-defined conversion 推导冲突参数。
↡从调用实参直接推断模板参数的阶段,该阶段通常不执行用户定义隐式转换来完成推导。template<class T>
Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs);Rational<int> * 2 可能因第二参数推导为 int、第一参数推导 T=int 的形式冲突而失败。可使用 hidden friend、non-deduced context、mixed-type template 或 targeted overload;这是 Item 46 还会深入的主题。
不能把非模板函数的转换直觉原样套到 template。
implicit maybe如整数到有理数,语义唯一且不丢信息。
explicit/named货币、角度、时间单位不能静默猜测。
reject范围或精度损失必须显式检查并报告失败。
member +=左对象身份被修改,不要求两侧完全对称。
free * / +两侧地位相同并允许相同安全转换。
overload/concept推导阶段不靠用户转换,需单独设计泛型入口。
Lab
Item 24 转换对称性实验
先预测候选、转换和封装边界,再切换对称值运算、显式单位和成员不对称样本。
Rational * int 与 int * Rational 都进入同一 non-member candidate
operator*=member → operator*=free → lhs/rhs=ordinary-parameters → safe-conversion
判定
accept:两侧转换规则对称
当前样本:对称值运算;保存表达式、候选、conversion sequence、单位假设与复位轨迹。
用左右矩阵验收二元接口
先预测每个表达式收集哪些 candidates、每个参数使用何种 conversion sequence,再建立测试:
Rational * Rational、Rational * int、int * Rational三组 compile tests。- unsafe unit/narrowing conversion 必须 compile fail 或返回显式错误。
- overload ambiguity tests 覆盖 int、long、double 与 wrapper combinations。
- ADL tests 确认 operator 位于类型 namespace,global/
std不被污染。 - encapsulation audit 确认 operator 能否保持 non-friend。
- algebra tests 验证左右形式值一致、identity/invariant 正确。
- template tests 单独覆盖 deduction,不用普通 implicit conversion 测试替代。
对称性是 API 语义,不只是两条示例能编译。
小结
- member operator 把 lhs 作为 implicit object parameter,导致 rhs 可转换而 lhs 不能先转换后发现成员
- non-member function 让所有 operands 成为普通参数,使安全 type conversions apply to all parameters
- operator 与类型放同 namespace,借助 ADL 发现;能用 public API 时保持 non-friend
- implicit conversion 只适用于无损、唯一且不需上下文的语义变化
- mutating compound operator 通常是 member,对称 value operator 可基于它实现为 free function
- function template deduction 不自动复用普通函数的转换规则,需要独立设计和测试
名词解释
本章出现的专业名词,用大白话再讲一遍。
- converting constructor
可用于用户定义隐式转换的单参数构造入口。
- overload candidate formation
收集候选并检查参数绑定的过程。
- user-defined implicit conversion
编译器插入 constructor 或 conversion function。
- ordinary function parameter
显式位于函数参数列表的参数。
- implicit object parameter
member 调用中由 this 表示的 receiver。
- member conversion asymmetry
this 与普通参数获得不同转换待遇。
- non-member function
所有 operands 显式出现在参数列表的自由函数。
- symmetric parameter conversion
左右参数具有相同转换资格。
- argument-dependent lookup
按实参关联范围补充函数候选。
- associated namespace set
ADL 搜索的参数关联 namespace/class 集合。
- public-contract operator
只依赖公开契约的 non-member operator。
- narrow operator friendship
向特定 operator 授予最小 private 权限。
- semantics-preserving conversion
无损、无歧义且保持不变量的转换。
- explicit conversion boundary
调用点必须明确写出的转换。
- mutating compound operator
修改 receiver identity 的复合运算。
- compound-operator reuse
free binary operator 复用 compound member 的模式。
- targeted mixed-type overload
为特定混合参数组合声明的 overload。
- overload ambiguity
多个候选 conversion rank 相同而无法选择。
- operator encapsulation boundary
class 与获权 operator 的共同维护范围。
- template argument deduction boundary
模板推导不依靠用户转换补全参数的边界。
- staged generic resolution
分别处理推导、转换与 overload 的设计。
- operand conversion matrix
左右类型组合及候选转换的验证表。
练习
- 问题 1(type conversions apply to all parameters、non-member function):解释 Rational 失败。 member operator 下
r * 2成功、2 * r失败,请写出候选和转换链并修复。
- 问题 2(implicit conversion、operator、乘法操作符):审查 Money 转换。* 团队想让 int 自动变 Money,以支持
money + 10与10 + money,请判断。
- 问题 3(non-member function、implicit conversion):处理 Rational template。 普通函数版本支持
Rational<int> * 2,改成 function template 后推导失败,设计验证方案。