Item 46:需要类型转换时在 template 内定义 non-member function
对齐 Effective C++ 第三版 Item 46:解释 Rational 混合乘法为何在 template argument deduction 阶段拒绝隐式转换,并通过 class template 内定义 hidden friend 让 ADL 找到具体非模板函数,再执行对称类型转换。
学习目标
- 能复现
Rational<int> * int的 function template deduction 失败,并解释用户定义转换为何尚未参与 - 能实现 class template 内定义的 non-template friend operator,让 ADL、普通 overload resolution 和隐式转换按顺序工作
- 能设计对称操作、helper factoring 与正反编译测试,避免链接错误、过宽转换和重复重实现
转换阶段实验
先预测:转换在哪一步才会发生?
先预测 deduction、ADL 和 overload conversion 的顺序,再切换场景查看证据。
观察
Rational<int> * 2 先要形成 operator<T> specialization;第二参数 int 不能在 deduction 阶段靠用户定义转换匹配 Rational<T>。
决策
把 deduction failure 与 ordinary overload conversion 分成两个阶段;不要要求客户显式写 operator*<int>。
当前场景 · template argument deduction / type conversions are desired
把 deduction failure 与 ordinary overload conversion 分成两个阶段;不要要求客户显式写 operator*<int>。
从一个“明明能转换”的失败开始
template<class T>
class Rational {
public:
Rational(const T& numerator = 0, const T& denominator = 1)
: numerator_(numerator), denominator_(denominator) {}
private:
T numerator_;
T denominator_;
};
template<class T>
Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs);Rational<int> oneHalf(1, 2);
auto value = oneHalf * 2; // deduction failureRational<int> 有可接收 int 的 non-explicit constructor,直觉上 2 可转换为 Rational。但调用仍失败。
Item 46 的原则是 Define non-member functions inside templates when type conversions are desired(需要类型转换时在模板内定义非成员函数)。
先预测:第一参数已经把 T 推成 int,为什么第二参数仍不能先转成 Rational?因为 deduction 要先让整个 function template specialization 成为候选。
deduction 与 conversion 有严格顺序
对第一参数,模式 const Rational<T>& 与 Rational<int> 匹配,得到 T=int。对第二参数,模式仍是 const Rational<T>&,实际是 int;template argument deduction 不考虑用户定义转换来弥合这个差异。
即使另一参数似乎已经确定 T,deduction 仍按规则检查参与推导的参数;不能把普通函数调用的 conversion 规则提前借给 template deduction。
为什么 operator 应是 non-member
若把 multiplication 写成 member:
template<class T>
class Rational {
public:
Rational operator*(const Rational& rhs) const;
};oneHalf * 2 可把 rhs 转为 Rational,但 2 * oneHalf 不可行,因为左 operand 不是 Rational,无法选择 member operator。
non-member operator 让两个 operands 都是普通函数参数,因此两侧都可转换。这延续 Item 24 的接口原则,但 class template 又增加 deduction 问题。
在 class template 内定义 non-template friend
template<class T>
class Rational {
public:
Rational(const T& n = 0, const T& d = 1) : n_(n), d_(d) {}
friend Rational operator*(const Rational& lhs, const Rational& rhs) {
return Rational(lhs.n_ * rhs.n_, lhs.d_ * rhs.d_);
}
private:
T n_;
T d_;
};当 compiler 实例化 Rational<int>,会形成具体函数:概念上是接收两个 const Rational<int>& 的 ordinary non-template operator。它不再需要推导 T。
ADL 如何找到 hidden friend
oneHalf * 2 等价于无限定 operator 调用。Rational 是一个 operand 的类型,argument-dependent lookup 会检查其关联 class,在其中找到 friend operator。
候选确定为 operator*(Rational<int>, Rational<int>) 后,普通 overload resolution 才为第二参数采用 Rational<int>(2) conversion。
auto a = oneHalf * 2; // rhs converts
auto b = 2 * oneHalf; // lhs converts这同时恢复了两个 operand directions。
声明 friend 不等于定义可链接函数
friend Rational operator*(const Rational&, const Rational&);只有 declaration 时,编译可能通过,但最终需要对应 concrete definition;若 namespace function template 与这个 non-template declaration 不是同一实体,容易产生 undefined reference。
↡候选和调用编译成功,但 concrete friend 缺少函数体导致的链接失败。Item 46 强调在 template 内定义 non-member function,确保每个使用到的 class specialization 都提供对应 ordinary friend body。class 内定义的 friend 隐含 inline,可跨 translation units 合法出现等价定义。
↡class definition 内定义的函数可在多个 translation units 出现而遵守 ODR 的属性。friend 不必直接承载重算法
若 operation 很重,可让 thin friend 调用 namespace helper template。
template<class T>
Rational<T> doMultiply(const Rational<T>& lhs, const Rational<T>& rhs) {
return Rational<T>(
lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}
template<class T>
class Rational {
public:
friend Rational operator*(const Rational& lhs, const Rational& rhs) {
return doMultiply(lhs, rhs);
}
};friend 负责 ADL 与 conversion boundary,helper 负责算法。这样也能减少每个 class body 中的大段实现,便于单独测试。
helper 的访问边界
helper 不是 friend,默认不能读取 Rational private state。可以让 Rational 提供 public observers,或把 helper 也声明为 friend template;前者通常耦合更小。
↡friend wrapper 通过 public observers 向 helper 提供数据而不扩大 private 访问的设计。const T& numerator() const noexcept { return n_; }
const T& denominator() const noexcept { return d_; }若 operation 必须维护 private canonical form,friend 可先校验/归一化,再调用低层 helper。
conversion constructor 的 explicit 策略
本例依赖 int 到 Rational 的隐式 conversion,因此 constructor 必须允许它。若 constructor 是 explicit,hidden friend 仍被找到,但普通参数 binding 不会自动构造 Rational。
↡是否允许 scalar 自动形成 domain value,从而参与对称 non-member operations 的接口选择。这不是要求所有 single-argument constructors 都 non-explicit。只有当 scalar 到 Rational 是自然、无损且符合领域语义时才开放;否则提供显式 factory 或混合类型 overload。
mixed underlying types 是另一个问题
Rational<int> * Rational<double> 不是 Item 46 这套同 T conversion 自动解决的场景。两个 class specializations 各生成自己的 same-T friend,目标公共类型需要额外设计。
可声明受约束的 heterogeneous operator template,结果使用 common_type_t<T,U>;但它重新进入 function template deduction,需要明确 constraints、promotion、precision 和 overload ambiguity。
template<class T, class U>
auto multiply(const Rational<T>& lhs, const Rational<U>& rhs)
-> Rational<std::common_type_t<T, U>>;不要把 same-T scalar conversion 与 heterogeneous promotion 混在一个无边界模板里。
验证顺序而不是只看最终成功
测试矩阵至少包括:
- Rational 与 Rational 同类型运算。
- Rational 与 scalar 两种 operand orders。
- 不可转换 scalar 被拒绝。
- explicit conversion policy 下自动调用被拒绝。
- helper definition 跨多个 translation units 无 ODR/link 错误。
- mixed underlying types 按明确 policy 接受或拒绝。
先预测失败发生在 lookup、deduction、conversion 还是 link,再执行 compile-fail 与运行测试;错误阶段本身就是对语言模型的验收。
小结
- function template argument deduction 不使用用户定义转换来匹配参数模式
- Rational 的 namespace operator template 因 scalar 参数无法完成 deduction
- class template 内定义的 non-template friend 随 specialization 形成 concrete ordinary function
- ADL 找到 hidden friend 后,普通 overload resolution 才能对任一 operand 执行 Rational conversion
- friend declaration 必须有对应定义;class 内定义隐含 inline,可避免 link gap
- thin friend 可把重算法委托 helper,同时保留对称转换和清晰封装
名词解释
本章出现的专业名词,用大白话再讲一遍。
- template argument deduction
从调用反推函数模板参数。
- user-defined conversion
constructor 或 conversion function 提供的转换。
- deduction-before-conversion
先推导候选再执行普通转换。
- deduction candidate failure
参数模式无法直接匹配导致候选失败。
- symmetric operand conversion
左右 operands 以相同规则参与转换。
- friend function
class 内声明但自身不是 member 的函数。
- argument-dependent lookup
按实参关联作用域扩展查找。
- specialization-bound ordinary function
绑定具体 class specialization 的非模板函数。
- friend definition link gap
声明可调用但缺 concrete 函数体的链接问题。
- implicit inline friend
class 内 friend 定义的 inline 属性。
- friend implementation helper
承载重实现的外部辅助模板。
- observer-based helper boundary
经 public observers 向 helper 提供数据。
- domain conversion policy
领域值是否允许自动构造的策略。
- heterogeneous rational operation
不同底层参数之间的有理数运算。
练习
- 问题 1:template argument deduction 与 type conversions are desired。 解释
oneHalf * 2的失败阶段,namespace 中只有 operator function template。
- 问题 2:non-member functions inside templates 与 friend function。 要支持两个 operand orders 且跨 translation units 正确链接。
- 问题 3:friend function 的 helper 边界与 template argument deduction。 operator 实现很大且需要维护 normalized invariant,设计 friend/helper 边界。