C++编程中的类型安全与显式类型转换

在超过20年的专业编程生涯中,深刻体会到隐式类型转换和不严谨的函数签名结合使用时,可能导致的错误是多么难以发现。如果没有通过显式类型转换强制执行的强类型安全,编译器不会在参数顺序混乱、使用错误的测量单位,或者出现其他不幸的混合错误时提供帮助。

案例研究:考虑一个计算圆扇形面积的函数。公式如下:

A = (r^2 * theta) / 2

其中,A 是面积,r 是半径,θ 是圆心角的弧度。可以这样实现:

namespace bad { double circular_sector_area(const double& theta, const double& r) { return r*r*theta/2.0; } }

函数 double circular_sector_area(const double& theta, const double& r) 依赖于匿名量。参数 theta 是最容易出错的。没有信息表明它是圆扇形的圆心角,也没有关于函数期望的测量单位的信息。只有通过检查实现并需要了解几何学才能识别公式,才能确定 theta 是圆心角的弧度。参数 r,半径,与返回结果之间的关系可能不那么容易出错,但缺乏清晰度。

实现可以这样改进:

namespace less_bad { using meter_type = double; using radian_type = double; using square_meter_type = double; square_meter_type circular_sector_area(const radian_type& central_angle, const meter_type& radius) { return radius*radius*central_angle/2.0; } }

函数 square_meter_type circular_sector_area(const radian_type& central_angle, const meter_type& radius) 提供了关于参数和返回值的信息。参数名称清楚地说明了它们代表什么,并且使用类型定义给出了预期的测量单位。信息就在那里,供任何愿意阅读函数签名的人阅读。然而,类型定义并不明确。可能会搞错测量单位和/或参数顺序。

如果尝试使用基本类型作为基类,会痛苦地发现这是不允许的。在命名空间 wrong 中创建类 radian_type 就像上面那样会导致编译器错误(MSVC++ 会告诉错误 C3770: 'double':不是一个有效的基类)。

这个案例研究中的最终实现使用了类模板,用于专门化基本类型:

namespace better { struct radian_type_tag {}; using radian_type = go::type_traits::fundamental_type_specializer; struct degree_type_tag {}; using degree_type = go::type_traits::fundamental_type_specializer; struct meter_type_tag {}; using meter_type = go::type_traits::fundamental_type_specializer; struct square_meter_type_tag {}; using square_meter_type = go::type_traits::fundamental_type_specializer; square_meter_type circular_sector_area(const radian_type& central_angle, const meter_type& radius) { return square_meter_type(((radius*radius).get()*central_angle.get())/2.0); } square_meter_type circular_sector_area(const degree_type& central_angle, const meter_type& radius) { static const double pi = std::acos(-1.0); return square_meter_type(((radius*radius).get()*central_angle.get())*pi/360.0); } }

命名空间 better 中的函数通过使用专门化类型提供预期的测量单位信息。仍然可以搞错一切,但必须更加努力。专门化类型是明确的,即,不能将匿名 double 或 degree_type 值传递给期望 radian_type 参数的函数,也不能搞错参数顺序(对于连续的相同类型参数除外)。

使用代码:专门化基本类型的类模板在 中声明。它需要每个专门化类型的唯一分派标签。为了方便和清晰,建议为专门化的基本类型声明类型别名或 typedef 名称。

#include #include struct category_type_tag {}; using category_type = go::type_traits::fundamental_type_specializer; GO_IMPLEMENT_FUNDAMENTAL_TYPE_SPECIALIZER(product_id_type, unsigned int) struct product { category_type category; product_id_type id; std::string name; };

在上面的代码中,声明了两个专门化的基本类型。首先 category_type 使用实际代码声明(第 4 到 6 行)。其次 product_id_type 使用宏 GO_IMPLEMENT_FUNDAMENTAL_TYPE_SPECIALIZER 声明,它创建了分派标签和类型别名(第 8 行)。

类模板 fundamental_type_specializer 实现了所有相关的运算符,取决于它专门化的基本类型。

赋值运算符:

a = b a += b a -= b a *= b a /= b a %= b a &= b a |= b a ^= b a <<= b a >>= b

算术运算符:

+a -a a + b a - b a * b a / b a % b ~a a & b a | b a ^ b a << b a >> b

比较运算符:

a == b a != b a < b a > b a <= b a >= b a <=> b

递增/递减运算符:

++a --a a++ a--

逻辑运算符:

!a a && b a || b

专门化的类是明确的。当需要访问包含的基本类型时,必须使用 get 或 set 函数。

有趣的点:SFINAE 和类型特征

"Substitution Failure Is Not An Error" SFINAE 是一个 C++ 缩写,代表 Substitution Failure In Not An Error。这意味着当编译器无法替换模板参数时,它不应该放弃,而是继续寻找有效的匹配。SFINAE 策略用于消除无效的模板参数是旧式的 C++98。SFINAE 与 C++11 中引入的新类型特征的结合非常强大。

template class fundamental_type_specializer : detail::fundamental_type_specializer_base { public: using this_type = fundamental_type_specializer; using fundamental_type = FundamentalType; using this_const_reference = const this_type&; // ... template constexpr typename std::enable_if::value, this_type>::type operator%(this_const_reference t) const noexcept { return this_type(std::forward(this->_t % t._t)); } template constexpr typename std::enable_if::value, this_type>::type operator%(this_const_reference t) const noexcept { return this_type(std::forward(std::fmod(this->_t, t._t))); } // ... private: fundamental_type _t; };

上面的代码片段显示了如何使用类型特征辅助类(在标准库头文件 中找到)来实现不同版本的模运算符,取决于专门化的基本类型,一个是整数类型(第 12 到 17 行),另一个是浮点类型(第 19 到 24 行)。实现使用了类型特征辅助类:

std::enable_if std::is_integral std::is_floating_point

std::enable_if 是一个元函数。它通过 SFINAE 和类型特征提供了一种方便的方法,通过条件地移除函数并提供单独的函数重载。

template struct enable_if;

std::is_integral 是一个元函数,它检查 T 是否是整数类型。如果 T 是类型 bool, char, char8_t, char16_t, char32_t, wchar_t, short, int, long, 或 long long,包括有符号和无符号变体,则返回 true。否则,返回 false。std::is_integral 可以支持其他实现定义的整数类型。

std::is_floating_point 是一个元函数,它检查 T 是否是浮点类型。如果 T 是类型 float, double, 或 long double,则返回 true。否则,返回 false。

中还有更多的辅助类元函数。类模板 fundamental_type_specializer 的实现还使用了 std::is_signed。

三向比较运算符:"Spaceship Operator"

随着 C++20 的到来,除了其他新特性外,还有三向比较运算符 <=>。它是所有其他比较运算符的通用泛化。实现了 <=> 的类会自动获得编译器生成的运算符 <, <=, >, 和 >=。运算符 <=> 可以默认定义。

class example { public: constexpr auto operator<=>(const example&) const noexcept = default; };
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485