在超过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 参数的函数,也不能搞错参数顺序(对于连续的相同类型参数除外)。
使用代码:专门化基本类型的类模板在
#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;
};
上面的代码片段显示了如何使用类型特征辅助类(在标准库头文件
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。
三向比较运算符:"Spaceship Operator"
随着 C++20 的到来,除了其他新特性外,还有三向比较运算符 <=>。它是所有其他比较运算符的通用泛化。实现了 <=> 的类会自动获得编译器生成的运算符 <, <=, >, 和 >=。运算符 <=> 可以默认定义。
class example {
public:
constexpr auto operator<=>(const example&) const noexcept = default;
};