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 才能成立。

member operator 把左 operand 隐藏为 this;右参数可转换,并不意味着左 receiver 也有同等待遇。

非成员函数让两侧地位相同

把乘法移出 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(所有参数参与类型转换)。

non-member function 让左右 operands 都成为普通参数,候选收集后可对两侧应用相同转换规则。

ADL 帮助发现同命名空间操作符

2 * oneHalf,普通查找与 argument-dependent lookup 会考虑 Rational 所在 namespace 的 non-member operator。

只要一个 operand 已是 Rational,ADL 可找到同 namespace operator;另一个 operand 再接受安全转换。若两个 operands 都是 int,则没有 Rational associated namespace,也不应把普通整数乘法劫持成 Rational。

函数应与类型放同 namespace,而不是散落 global scope 或添加到 std

通常不需要 friend

Item 24 要求 non-member,不等于要求 friend。若 public accessors 足以实现乘法,应保持 non-member non-friend,减少 private representation 依赖。

如果 performance/invariant 真正要求直接访问,可考虑 narrow friend;但需要把它计入封装边界。不能因为 operator 写在 class 附近就自动 friend。

只有安全转换才应隐式发生

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 和重复实现。

优先从领域 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。

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 conversion 仍必须由领域信息损失和歧义决定。

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 * RationalRational * intint * 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 不自动复用普通函数的转换规则,需要独立设计和测试

资料与写作方式声明

本章以Effective C++, Third Edition, Item 24权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

名词解释

本章出现的专业名词,用大白话再讲一遍。

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 相同而无法选择。

hidden friend

class 内定义、主要由 ADL 找到的 non-member friend。

operator encapsulation boundary

class 与获权 operator 的共同维护范围。

template argument deduction boundary

模板推导不依靠用户转换补全参数的边界。

staged generic resolution

分别处理推导、转换与 overload 的设计。

operand conversion matrix

左右类型组合及候选转换的验证表。

练习

  1. 问题 1(type conversions apply to all parameters、non-member function):解释 Rational 失败。 member operator 下 r * 2 成功、2 * r 失败,请写出候选和转换链并修复。
  1. 问题 2(implicit conversion、operator、乘法操作符):审查 Money 转换。* 团队想让 int 自动变 Money,以支持 money + 1010 + money,请判断。
  1. 问题 3(non-member function、implicit conversion):处理 Rational template。 普通函数版本支持 Rational<int> * 2,改成 function template 后推导失败,设计验证方案。

讨论

评论区加载中…