彻底理解c++的隐式类型转换 (5)

这样的隐式转换带来的坏处是什么呢?答案是数组的长度丢失了。假如你不知道这一点,在函数中仍然用sizeof去求数组的大小,那么难免不会出问题。

解决办法有很多,比如最简单的借助模板:

template <std::size_t N> void func(int (&arr)[N]) { std::cout << (sizeof arr) << std::endl; // 400 std::cout << N << std::endl; // 100 }

现在N是100,而sizeof会返回400,因为sizeof一个引用会返回引用指向的类型的大小,这里是int [100]。

一个更简单也更为现代c++推崇的做法是放弃原始数组,把它当做沉重的历史包袱丢弃掉,转而使用std::array和即将到来的std::span。这些更现代化的数组替代品可以更好得代替原始数组而不会发生诸如隐式转换成指针等问题。

两步转换

还有不少教程会告诉你在隐式转换的时候超过一次的类型转换是不可以的,我习惯把这种问题叫做“两步转换”。

为什么叫两步转换呢?假如我们有ABC三个类型,A可以转B,B可以转C,他们是单步的转换,而如果我们需要把A转成C,就需要先把A转成B,因为A不能直接转换成C,因此形成了一个转换链:A -> B -> C,其中进行了两次类型转换,我称其为两步转换。

下面是一个典型的“两步转换”:

struct A{ A(const std::string &s): _s{s} {} std::string _s; }; void func(const A &s) { std::cout << s._s << std::endl; } int main() { func("two-steps-implicit-conversion"); }

我们知道const char*能隐式转换到string,然后string又可以隐式转换成A:const char* -> string -> A,而且函数参数是个常量左值引用,应该能绑定到隐式转换产生的右值。然而用g++编译代码会是下面的结果:

test.cpp: In function 'int main()': test.cpp:15:10: error: invalid initialization of reference of type 'const A&' from expression of type 'const char [30]' 15 | func("two-steps-implicit-conversion"); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test.cpp:8:20: note: in passing argument 1 of 'void func(const A&)' 8 | void func(const A &s) | ~~~~~~~~~^

果然报错了。可是这真的是因为两步转换带来的结果吗?我们稍微改一改代码:

struct A{ A(bool b) { _s = b ? "received true" : "received false"; } std::string _s; }; void func(const A &s) { std::cout << s._s << std::endl; } int main() { int num = 0; func(num); // received false unsigned long num2 = 100; func(num2); // received true }

这次不仅编译通过,而且指定-Wall -Wextra也不会有任何警告,输出也是正常的。

那就怪了,这里的两次调用分别是int -> bool -> A和unsigned long -> bool -> A,很明星的两步转换,怎么就是合法的正常代码呢?

其实答案早在那节就告诉过你了:

一个隐式类型转换序列包括一个初始标准转换序列、一个用户定义转换序列、一个第二标准转换序列

也就是说不存在什么两步转换问题,本身转换序列最少可以转换1次,最多可以三次。两次转换当然没问题了。

唯一会触发问题的是出现了两次用户定义转换,因为隐式转换序列里只允许一次用户定义转换,语言标准也规定了不允许出现多余一次的用户定义转换:

At most one user-defined conversion (constructor or conversion function) is implicitly applied to a single value. -- 12.3 Conversions [class.conv]

所以这条转换链:const char* -> string -> A 是有问题的,因为从字符串字面量到string和string到A都是用户定义转换。

而int -> bool -> A和unsigned long -> bool -> A这两条是没问题的,第一次转换是初始标准转换序列完成的,第二次是用户定义转换,整个过程合情合理。

由此看来教程们只说对了一半,“两步转换”的症结在于一次隐式转换中不能出现两次用户定义的类型转换,这个问题叫做“两步自定义转换”更恰当。

用户定义的类型转换只能出现在自定义类型中,这其中包括了标准库。所以换句话说,当你有一条A -> B -> C这样的隐式转换链的时候,如果其中有两个都是自定义类型,那么这个隐式转换是错误的。

唯一的解决办法就是把第一次发生的用户自定义转换改成显式类型转换:

struct A{ A(const std::string &s): _s{s} {} std::string _s; }; void func(const A &s) { std::cout << s._s << std::endl; } int main() { func(std::string{"two-steps-implicit-conversion"}); }

现在隐式转换序列里只有一次自定义转换了,问题也就不会发生了。

总结

相信现在你已经彻底理解c++的隐式类型转换了,常见的坑应该也能绕过了。

但我还是得给你提个醒,尽量不要去依赖隐式类型转换,多用explicit和各种显式转换,少想当然。

Keep It Simple and Stupid.

参考资料

https://zh.cppreference.com/w/cpp/language/copy_elision

https://en.cppreference.com/w/cpp/language/implicit_conversion

https://stackoverflow.com/questions/26954276/second-standard-conversion-sequence-of-user-defined-conversion

https://stackoverflow.com/questions/48576011/why-does-const-allow-implicit-conversion-of-references-in-arguments/48576055

https://zh.cppreference.com/w/cpp/language/cast_operator

https://www.nextptr.com/tutorial/ta1211389378/beware-of-using-stdmove-on-a-const-lvalue

https://en.cppreference.com/w/cpp/language/reference_initialization

https://stackoverflow.com/questions/12847272/multiple-implicit-conversions-on-custom-types-not-allowed

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wsxssx.html