强类型是 C++ 社区中的热门话题。在这篇文章中,我想特别关注如何使用它们来使界面更清晰、更健壮。
动机
首先,什么是强类型?强类型是一种用于代替另一种类型的类型,通过其名称传递特定含义。与强类型相反的是通用类型,例如 int 和 double 等原生类型。通常,原生类型不会过多地说明其实例的含义。
为了说明这一点,让我们以类 Rectangle 为例。假设一个 Rectangle 可以用宽度和高度初始化。要将其写成接口,首先想到的是使用 double:
class Rectangle
{
public:
Rectangle(double width, double height);
....
};
double 是一个通用类型,根据上面的定义,它并不是强类型。但是这块带来来看,好像没什么问题。
使用通用类型真正的问题出现在调用端,当我们调用上述接口:
Rectangle r(10, 12);
对于调用构造函数的读者来讲,10 和 12 并没有指示哪一个是宽度或是高度。这迫使读者去检查 Rectangle 的接口。它可能位于另一个文件中。因此,这种通用类型并不具备可读性:代码非常清楚 10 是宽度,12 是高度,但它不会告诉你。
此外,使用 double 的 Rectangle 接口还有另一个问题:调用端可能以错误的顺序传递参数。例如:
Rectangle r(12, 10); // 糟糕,本意是将宽度设置为 10,但混淆了参数
创建强类型
要解决代码的迷惑性,一个解决方案是在调用端解决代码的混淆性。
这就是强类型要做的。在 第一篇 中,我们碰到了需要在接口的某些部分写出名字的问题。在构造函数的特殊情况下,我们围绕原生类型构造了一个薄包装,其唯一目的是指定一个特定的名称。为了表达一个特定的 double 代表半径,我们编写了以下包装器:
class Radius
{
public:
explicit Radius(double value) : value_(value) {}
double get() const { return value_; }
private:
double value_;
};
现在很明显,这个包装中没有任何可以通用的东西。因此,编译一个组件来包装特定类型 T 是很自然的事情。让我们称这个组件为 NamedType:
template <typename T>
class NamedType
{
public:
explicit NamedType(T const& value) : value_(value) {}
explicit NamedType(T&& value) : value_(std::move(value)) {}
T& get() { return value_; }
T const& get() const {return value_; }
private:
T value_;
};
TIPS: 最终实现请参阅帖子底部
double 现在已经被类型 T 取代了。但是实参和返回值并不是,因为他们是按值传递的。更通用的情景下,实参应当使用由 const 限定的引用传递。
有几个方式可以为特定类型实例化 named type,但是我发现下面的完全没有歧义:
using Width = NamedType<double>;
一些实现使用了继承。但是我发现上面的代码更具有表达型,因为它表示我们只是需要在类型上添加一个标签。
使用幻影类型
如果仔细思考以下,上面的实现根本就不是通用的。实际上,如果使用特定的类型来表达高度,您会怎么做呢?如果您执行了以下操作:
using Height = NamedType<double>;
我们又回到了原点:Width 和 height 只是 NamedType<double> 的两个别名,因而他们可以互换,这与我们的意图相驳。
为了处理这个问题,我们可以添加一个参数,它用于区分每一个 named type,。比如一个参数用于 Widget,另一个用于 Height 等等。
换句话说。我们想要参数化 类型 NamedType。在 C++ 中,参数化类型可以通过传递模板参数实现:
template <typename T, typename Parameter>
class NamedType
{
....
}
实际上,Parameter 并没有在 NamedType 的实现中使用。这就是为什么叫它 幻影类型 。
在这里,我们希望 NamedType 的每个实例化都有一个模板参数,该参数在整个程序中都是唯一的。这可以通过每次定义一个专用类型来实现。由于创建此专用类型的唯一目的是作为模板参数传递,因此它不需要任何行为或数据。我们称它为 WidthParameter,用于 Width 的实例化:
struct WidthParameter {};
using Width = NamedType<double, WidthParameter>;
实际上,WidthParameter 能够在 using 语句的同行声明,这使实例化强类型 只在一行代码 提供了可能性:
using Width = NamedType<double, struct WidthParameter>;
对于 Height 而言:
using Height = NamedType<double, struct HeightParameter>;
现在 Width 和 Height 都有了明确的名称,而且实际上是两种不同的类型。
使用这些重写 Rectangle 接口:
class Rectangle
{
public:
Rectangle(Width, Height);
....
};
Note: 参数名已经不需要了,因为类型已经提供了所有信息。
在调用端,可以用下面的方式:
Rectangle r(Width(10), Height(12));
强类型和自定义字面量
上述的代码可以和自定义字面量一起工作。现假设用 meter 来表示以米为单位的距离。meter 只是一个用来表示距离的数字,但是使用 NamedType 来表示:
using Meter = NamedType<double, struct MeterParameter>;
NamedType 可以被结合,因而可以用来表示宽和高:
using Width = NamedType<Meter, struct WidthParameter>;
using Height = NamedType<Meter, struct HeightParameter>;
如果我们为 meter 添加一个自定义字面量:
Meter operator"" _meter(unsigned long long length)
{
return Meter(length);
}
TIPS: 为了适应所有情况,同时应当为 long double 添加重载。
然后我们可以用这种方式调用:
Rectangle r(Width(10_meter), Height(12_meter));
结论和展望
强类型强迫接口更富表达力,尤其是在调用端。而且通过强制正确的参数顺序来减少出错以增强接口。它们可以用下面的包装器实现:
template <typename T, typename Parameter>
class NamedType
{
public:
explicit NamedType(T const& value) : value_(value) {}
explicit NamedType(T&& value) : value_(std::move(value)) {}
T& get() { return value_; }
T const& get() const {return value_; }
private:
T value_;
};
然后他们可以以这种方式使用:
using Width = NamedType<double, struct WidthParameter>;
要深入了解这个有用且流行的主题。你可以探索这些方面:
在 Simplify C++ 上使用强类型执行业务。
在 foonathan::blog() 上以模块化的方式为强类型提供更多功能。
在我这边,我将通过引用来介绍强类型的传递。实际上,上述所有实现在每次传递给接口时都发生了拷贝,但在某些情况下,这并不是您想要的。我还没有在任何地方看到过强类型的这一方面。所有这将是我们强类型系列下一篇文章的重点。