如何在C++中最佳实现“newtype”习语?

36
自学 Rust 后,我成为了 newtype 惯用语 的粉丝,我认为 Rust 从 Haskell 借鉴了这个惯用语。
新类型是一种基于标准类型的独立类型,它确保函数参数具有正确的类型。例如,下面的 old_enough 函数必须传入年龄。如果传入天数或纯 i64,则无法编译。
struct Days(i64);
struct Years(i64);

fn old_enough(age: &Years) -> bool {
    age.0 >= 18
}

这与C++中的typedefusing声明不同,它只是简单地重命名类型。
例如下面的old_enough函数将接受一个int、一个以Days为单位的年龄或任何其他可以转换为int的内容:

typedef int Days;
using Years = int;

bool old_enough(Years age) {
    return age >= 18;
}

正如上面的示例仅使用整数,这篇 Reddit 帖子建议使用枚举类,例如:

enum class Days : int {};
enum class Years : int {};

bool old_enough(Years age) {
    return static_cast<int>(age) >= 18;
}

或者它可以简单地使用结构,例如Rust:

struct Days final {int value;};
struct Years final {int value;};

bool old_enough(Years age) {
    return age.value >= 18;
}

什么是在C++中实现newtype惯用法的最佳方法?是否有标准方法?
编辑问题使用强类型和typedef类似。然而,它没有考虑到newtype惯用法。

7
在C++中,您可能会创建一个Year类,并在构造函数和/或赋值级别上进行验证。比如 auto year = new Year(2002) 然后 year.old_enough(),或者对于更严格的实现,当提供无效值时会throw异常。 - tadman
2
如果你真的只需要相同类型但是不同类型,那么它需要相当多的样板文件。幸运的是,有人已经写好了它,你可以使用 BOOST_STRONG_TYPEDEF,但总的来说我同意 tad 的观点,一个“年份”不是一个“整数”,它应该有自己的类型。 - 463035818_is_not_a_number
3
这个问题的答案是否能够回答你的疑问?[使用强类型的using和typedef] - underscore_d
3
你的意思是 auto year = Year(2002); - user253751
2
值得注意的是,对于这个特定的例子,C++标准库已经有了std::chrono::yearsstd::chrono::days,它们本质上已经是这种类型的包装器了。(尽管在这种情况下,还可以从例如std::chrono::daysstd::chrono::seconds的隐式转换,其背后的实现将通过正确的转换因子乘以该值。)Boost还有一个单位库,它处理将双精度值包装在SI(或英制单位)中,并在幕后进行转换。 - Daniel Schepler
显示剩余10条评论
3个回答

15
什么是在C++中实现"newtype"习惯用法的最佳方法?评价"最佳"往往会导致偏好领域,但您已经提到了两种替代方法:简单地使用自定义结构体包装一个常见类型(比如int),或者使用枚举类来明确指定强类型近似相同的类型的基础类型。如果你主要是想获得一个常见类型的强类型别名,则可以使用typedef或using语句。
struct Number { int value; }

或者,一种具有可参数化底层类型的常见类型

template<typename ValueType = int>
struct Number { ValueType value; }

另外一种常见的方法(也方便在强类型不同但相关的类型之间重复使用功能)是将 Number 类(模板)扩展为一个基于类型模板 tag 参数的类模板,这样在特化标签类型时就会产生强类型。如 @Matthieu M. 所指出的那样,我们可以将结构体声明为给定特化模板参数列表的一部分,从而允许在单个别名声明中进行轻量级标记声明和别名标记:

template<typename Tag, typename ValueType = int>
struct Number {
    ValueType value;
    // ... common number functionality.
};

using YearNumber = Number<struct NumberTag>;
using DayNumber = Number<struct DayTag>;

void takeYears(const YearNumber&) {}
void takeDays(const DayNumber&) {}

int main() {
    YearNumber y{2020};
    DayNumber d{5};
    
    takeYears(y);
    //takeDays(y);  // error: candidate function not viable
    
    takeDays(d);
    //takeYears(d);  // error: candidate function not viable
    
    return 0;
}

请注意,如果您想为特定标记专门化Number类模板的非成员函数(或例如使用标记调度实现类似的目的),则需要在别名声明之外声明类型标记。


3
你可以通过利用“inline”结构声明来减少重量,并避免使用“enum”:using YearNumber = Number<struct YearTag>; - Matthieu M.
1
@MatthieuM。这太棒了,我不知道你可以在模板参数列表中声明结构体作为类型模板参数。谢谢,我会尽快更新答案。 - dfrib
1
据我所知,匿名结构体必须在声明时被定义。我记得 WG21/DR 62 触及了这个问题(_"以下类型不得用作模板类型参数的模板实参:……没有名称用于链接目的的未命名类或枚举类型"_)。然而,我无法从 [temp.arg.type] 中找到一个 class-specifier 是一个有效的 _template-argument_,但是这显然可以工作(我尝试过的所有编译器都可以)。你有什么想法吗? - dfrib
@kenba 更新了一个可参数化的 ValueType 类型模板参数(但是没有对该类型进行 SFINAE 限制;超出范围)。 - dfrib
1
由于我无法找到标准文件中关于非类型模板参数的 template-argument 中 (不完整) 类声明的规定,因此我发布了一个问题,希望更有经验的语言专家能够解答。问题链接 - dfrib
显示剩余4条评论

4

我过去使用过 boost strong typedef。它的文档似乎非常简洁,但是就我所知,Facebook正在使用它,并且 LLVM 似乎有一个类似的东西叫做LLVM_YAML_STRONG_TYPEDEF,这表明它可能已经被一些真实世界的应用所采用。


2
如果你有,BOOST_STRONG_TYPEDEF已经实现了你想要的功能,就像这个答案中所看到的那样。
目前在C++语言中没有直接满足您要求的机制。但是,详细的需求可能会有所不同,例如,有人可能认为隐式构造是可以接受的,而另一个人可能认为必须是显式的。由于这种和其他组合的存在,很难提供一个能够满足所有人的机制,我们已经有了普通类型别名(即使用using,当然这与强类型定义不同)。
话虽如此,给了你足够的工具,你可以自己构建这个通用工具,如果你有一些等经验,这并不是完全困难的。
最终取决于您实际需要的新类型问题,例如,您只需要一些或者您将批量生成这些。对于像年份和天数这样普通的东西,您可以只使用裸的结构体。
struct Days {int value;};

struct Years {int value;};

然而,如果你必须避免这种情况:
bool isold(Years y);

...

isold({5});

您需要创建一个构造器并将其声明为明确的,例如:

struct Years {
   explicit Years(int i);
...

另一种组合可能是,如果新类型应该被允许转换为基础类型,这对于像 int 这样的东西可能很有用,但根据上下文可能也很危险。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接