Effective C++ 条款24:若所有参数皆须要类型转换,请为此采用 non-member 函数
Effective C 条款24若所有参数皆须要类型转换请为此采用 non-member 函数如果你需要为某个函数的所有参数包括被 this 指针所指的那个隐喻参数进行类型转换那么这个函数必须是个 non-member。一、引言一个令人困惑的编译错误假设你设计了一个有理数类Rational支持整数到有理数的隐式转换classRational{public:Rational(intnumerator0,intdenominator1);// 允许隐式转换intnumerator()const;intdenominator()const;// 乘法运算符——成员函数版本constRationaloperator*(constRationalrhs)const;private:intnumerator_;intdenominator_;};然后你写下这样的代码RationaloneHalf(1,2);Rational result;resultoneHalf*2;// ✅ 编译通过result2*oneHalf;// ❌ 编译错误为什么明明乘法应该满足交换律为什么oneHalf * 2可以而2 * oneHalf却不行二、问题根源成员函数的隐式转换不对称2.1 成员函数的本质当我们写oneHalf * 2时编译器实际上看到的是oneHalf.operator*(2);// 成员函数调用这里发生了隐式转换2是intoperator*的参数类型是const Rational编译器调用Rational(2)将int隐式转换为Rational最终等价于oneHalf.operator*(Rational(2))✅2.2 交换后的灾难当我们写2 * oneHalf时编译器看到的是2.operator*(oneHalf);// 试图在 int 上调用成员函数问题2是int类型不是Rationalint类没有operator*(const Rational)成员函数编译器不会将2先转换为Rational再调用成员函数因为成员函数的调用规则是左侧对象决定调用哪个类的成员函数核心原理成员函数的隐式转换只适用于参数右侧不适用于调用者左侧。this指针所指的对象不会参与隐式类型转换。三、解决方案non-member 函数实现对称转换3.1 正确的非成员实现classRational{public:Rational(intnumerator0,intdenominator1);intnumerator()const{returnnumerator_;}intdenominator()const{returndenominator_;}private:intnumerator_;intdenominator_;};// ✅ non-member 运算符——所有参数都参与隐式转换constRationaloperator*(constRationallhs,constRationalrhs){returnRational(lhs.numerator()*rhs.numerator(),lhs.denominator()*rhs.denominator());}现在RationaloneHalf(1,2);// ✅ 两侧都正确Rational result1oneHalf*2;// operator*(oneHalf, Rational(2))Rational result22*oneHalf;// operator*(Rational(2), oneHalf)Rational result32*3;// operator*(Rational(2), Rational(3))3.2 为什么 non-member 可以在 non-member 版本中operator*(lhs,rhs);两个参数都是显式列出的编译器会对所有参数进行隐式类型转换表达式转换过程oneHalf * 2operator*(oneHalf, Rational(2))2 * oneHalfoperator*(Rational(2), oneHalf)2 * 3operator*(Rational(2), Rational(3))四、深入理解this 指针的隐喻参数Scott Meyers 将this指针称为隐喻参数这是一个精妙的比喻// 成员函数版本classRational{constRationaloperator*(constRationalrhs)const;// 实际上等价于// const Rational operator*(const Rational* this, const Rational rhs);};对于成员函数参数列表中只有rhs一个显式参数。this是隐式的不参与隐式类型转换。而对于 non-member 函数// non-member 版本constRationaloperator*(constRationallhs,constRationalrhs);// 两个参数都是显式的都参与隐式类型转换五、实际应用场景5.1 数值类型的完整实现classRational{public:// 允许 int 到 Rational 的隐式转换Rational(intnumerator0,intdenominator1);// 显式转换到 double避免意外转换explicitoperatordouble()const{returnstatic_castdouble(numerator_)/denominator_;}intnumerator()const{returnnumerator_;}intdenominator()const{returndenominator_;}private:intnumerator_;intdenominator_;voidnormalize();// 约分};// ✅ 所有算术运算符都定义为 non-memberconstRationaloperator*(constRationallhs,constRationalrhs){returnRational(lhs.numerator()*rhs.numerator(),lhs.denominator()*rhs.denominator());}constRationaloperator(constRationallhs,constRationalrhs){returnRational(lhs.numerator()*rhs.denominator()rhs.numerator()*lhs.denominator(),lhs.denominator()*rhs.denominator());}constRationaloperator-(constRationallhs,constRationalrhs){returnRational(lhs.numerator()*rhs.denominator()-rhs.numerator()*lhs.denominator(),lhs.denominator()*rhs.denominator());}constRationaloperator/(constRationallhs,constRationalrhs){returnRational(lhs.numerator()*rhs.denominator(),lhs.denominator()*rhs.numerator());}// 比较运算符booloperator(constRationallhs,constRationalrhs){returnlhs.numerator()*rhs.denominator()rhs.numerator()*lhs.denominator();}booloperator(constRationallhs,constRationalrhs){returnlhs.numerator()*rhs.denominator()rhs.numerator()*lhs.denominator();}// 使用示例voidtestRational(){Rationala(1,2);Rationalb(1,3);// 所有混合运算都正确工作autoca*2;// Rational * intautod3*b;// int * Rationalautoe2*3;// int * int转换为 Rationalautofab*2;// 混合运算// 比较运算booleq(a1);// Rational intboollt(0b);// int Rational}5.2 物理量单位库classMeters{public:Meters(doublevalue0.0):value_(value){}doublevalue()const{returnvalue_;}private:doublevalue_;};classSeconds{public:Seconds(doublevalue0.0):value_(value){}doublevalue()const{returnvalue_;}private:doublevalue_;};classMetersPerSecond{public:MetersPerSecond(doublevalue0.0):value_(value){}doublevalue()const{returnvalue_;}private:doublevalue_;};// ✅ non-member 除法运算符——速度 距离 / 时间MetersPerSecondoperator/(constMetersdistance,constSecondstime){if(time.value()0){throwstd::invalid_argument(时间不能为零);}returnMetersPerSecond(distance.value()/time.value());}// 使用示例voidphysicsCalculation(){Metersdistance(100.0);Secondstime(10.0);// ✅ 两侧都可以是隐式转换结果autospeeddistance/time;// 10 m/s// 甚至可以这样如果定义了从 double 的隐式转换// auto speed2 100.0 / Seconds(10.0);}5.3 字符串拼接操作符classString{public:String(constchar*str);String(conststd::stringstr);constchar*c_str()const;size_tlength()const;private:std::string data_;};// ✅ non-member 拼接运算符Stringoperator(constStringlhs,constStringrhs){returnString(lhs.c_str()std::string(rhs.c_str()));}// 使用示例voidstringTest(){String s1Hello;String s2World;autos3s1s2;// String Stringautos4s1!;// String const char*autos5Hi s2;// const char* String ✅}六、常见误区与注意事项6.1 是否需要 friend答案通常不需要。// ✅ 不需要 friend——通过公有接口访问constRationaloperator*(constRationallhs,constRationalrhs){returnRational(lhs.numerator()*rhs.numerator(),// 通过 getter 访问lhs.denominator()*rhs.denominator());}只有当 non-member 函数必须访问私有成员且无法通过公有接口实现时才考虑 friend。但这种情况很少见。原则能用 non-member 就不用 friend。friend 的封装性比 member 还差。6.2 explicit 构造函数的影响classRational{public:explicitRational(intnumerator0,intdenominator1);// ❌ 阻止隐式转换// ...};// 现在以下代码全部失败Rational resultoneHalf*2;// ❌ 无法将 int 隐式转换为 RationalRational result22*oneHalf;// ❌ 同上如果你将构造函数标记为explicit那么所有隐式转换都会被阻止。这在某些情况下是想要的如防止意外的类型转换但意味着你需要显式构造Rational resultoneHalf*Rational(2);// ✅ 显式转换6.3 与条款23的协同条款24与条款23宁以 non-member non-friend 替换 member 函数完美协同条款23如果函数可以通过公有接口实现用 non-member 增加封装性条款24如果所有参数都需要类型转换用 non-member 实现对称性两者都指向同一个方向优先使用 non-member non-friend 函数。七、总结核心原则成员函数的隐式转换不对称只有右侧参数参与隐式转换this所指对象不参与non-member 实现对称转换所有显式参数都参与隐式类型转换不需要 friend通过公有接口即可实现大多数运算符算术运算符优先 non-member,-,*,/等应保持数学上的对称性快速决策表运算符推荐实现方式原因member必须是成员C语法[]member必须是成员C语法()member必须是成员C语法-member必须是成员C语法*/-non-member需要对称的类型转换!non-member需要对称的类型转换流non-member左侧是流对象不是自定义类型--前缀/后缀member需要修改对象状态最终建议classMyNumericType{public:// 构造函数——控制隐式转换MyNumericType(doublevalue0.0);// 访问函数doublevalue()const;private:doublevalue_;};// ✅ 算术运算符non-member支持对称转换MyNumericTypeoperator(constMyNumericTypelhs,constMyNumericTyperhs);MyNumericTypeoperator-(constMyNumericTypelhs,constMyNumericTyperhs);MyNumericTypeoperator*(constMyNumericTypelhs,constMyNumericTyperhs);MyNumericTypeoperator/(constMyNumericTypelhs,constMyNumericTyperhs);// ✅ 比较运算符non-memberbooloperator(constMyNumericTypelhs,constMyNumericTyperhs);booloperator(constMyNumericTypelhs,constMyNumericTyperhs);// ✅ 流运算符non-memberstd::ostreamoperator(std::ostreamos,constMyNumericTypeobj);std::istreamoperator(std::istreamis,MyNumericTypeobj);记住如果你需要为某个函数的所有参数包括隐喻的this参数进行类型转换那么这个函数必须是个 non-member。这是保证运算符对称性和类型系统灵活性的关键。参考与延伸阅读《Effective C》第三版Scott Meyers条款24《C Primer》第五版关于隐式转换和运算符重载的章节CppReference: Implicit conversions如果这篇文章对你有帮助欢迎点赞 、收藏 ⭐、留言 你的支持是我持续输出的动力