为什么std::unique_lock使用类型标记来区分构造函数

Why does std::unique_lock use type tags to differentiate constructors?

本文关键字:构造函数 类型 unique std lock 为什么      更新时间:2023-10-16

在c++ 11中,std::unique_lock构造函数被重载以接受类型标记defer_lock_t, try_to_lock_tadopt_lock_t:

unique_lock( mutex_type& m, std::defer_lock_t t );
unique_lock( mutex_type& m, std::try_to_lock_t t );
unique_lock( mutex_type& m, std::adopt_lock_t t );

这些是空类(类型标签),定义如下:

struct defer_lock_t { };
struct try_to_lock_t { };
struct adopt_lock_t { };

这允许用户通过传递这些类的预定义实例之一来消除三个构造函数之间的歧义:

constexpr std::defer_lock_t defer_lock {};
constexpr std::try_to_lock_t try_to_lock {};
constexpr std::adopt_lock_t adopt_lock {};

我很惊讶这不是作为enum实现的。据我所知,使用enum将:

  • 更容易实现
  • 不改变语法
  • 允许在运行时更改参数(尽管在这种情况下不是很有用)。
  • (可能)可以由编译器内联而不影响性能

为什么标准库使用类型标记而不是enum来消除这些构造函数的歧义?也许更重要的是,在编写自己的c++代码时,我是否也应该更喜欢在这种情况下使用类型标记?

标记调度

这是一种技术,称为标签调度。它允许在给定客户端所需的行为时调用适当的构造函数。

标记的原因是用于标记的类型因此是不相关的,并且在重载解析期间不会冲突。类型(而不是像枚举那样的值)用于解析重载函数。此外,标记可以用来解析调用,否则会有歧义;在这种情况下,标签通常基于一些类型特征。

使用模板的标签调度意味着只需要实现给定构造所需使用的代码。

标签调度允许更容易阅读代码(至少在我看来)和更简单的库代码;构造函数不会有switch语句,并且在执行构造函数本身之前,可以根据这些参数在初始化器列表中建立不变量。当然,你的里程可能会有所不同,但这是我使用标签的一般经验。

Boost.org有一篇关于标签调度技术的文章。它的使用历史似乎至少可以追溯到SGI STL。

为什么要用它?

为什么标准库使用类型标记而不是枚举来消除这些构造函数的歧义?

在重载解析和可能的实现中使用

类型将比枚举更强大和灵活;请记住,枚举最初是无作用域的,并且在如何使用它们方面受到限制(与标签相反)。

使用标签的其他值得注意的原因;

  • 编译时可以决定使用哪个构造函数,而不是运行时。
  • 禁止更多的"黑客";在代码中,一个整数被转换为枚举类型,其值没有被满足——需要做出设计决策来处理这个问题,然后实现代码来满足任何由此产生的异常或错误。
  • 请记住,shared_locklock_guard也使用这些标签,但在lock_guard的情况下,仅使用adopt_lock。枚举将引入更多潜在的错误条件。

我认为先例和历史在这里也起了作用。鉴于在标准库和其他地方的广泛使用;不太可能改变诸如原始示例之类的情况在库中实现的方式。

也许更重要的是,在编写自己的c++代码时,我是否也更喜欢在这种情况下使用类型标记?

这本质上是一个设计决策。两者都可以而且应该用来解决它们所解决的问题。我已经使用标签来"路由"。数据和类型到正确的功能;特别是当实现在编译时是不兼容的,如果有任何重载解析在起作用。

标准库std::advance通常作为一个例子,说明如何使用标签调度来实现和优化基于所使用类型的特征(或特征)的算法(在这种情况下,迭代器是随机访问迭代器)。

如果使用得当,它是一个强大的技术,不应该被忽视。如果使用枚举,请优先使用新范围的枚举,而不是旧的无范围的枚举。

使用这些标记使您能够利用该语言的类型系统。这与模板元编程密切相关。简单地说,使用这些标记可以在编译时静态地决定调用哪个构造函数。这为编译器优化留下了空间,提高了运行时效率,并使使用std::unique_lock的模板元编程更容易。这是可能的,因为标签是不同的静态类型。对于enum,不能这样做,因为enum的值在编译时无法预见。请注意,使用标记进行区分是一种常见的模板元编程技术。只要看看标准库使用的那些迭代器标签就知道了。

关键是,如果您想使用enum添加另一个函数,您应该编辑enum,然后重新构建使用您的函数和enum的所有项目。此外,将有一个函数以enum作为参数并使用switch或其他东西。这会给你的应用程序带来多余的代码。

否则,如果您使用带有标签的重载函数,则可以轻松地添加另一个标签并添加另一个重载函数,而无需更改旧的。这更加向后兼容。

我怀疑这是优化。注意,使用类型(按原样)时,在编译时选择了正确的版本。正如您所指出的,在运行时使用enum(可能)在某些条件语句(可能是switch)中选择。

在许多实现中,锁的获取和释放频率非常高,可能设计者认为分支预测和隐含的内存同步事件可能是一个重大问题。

我的论点中的缺陷(你也指出了)是构造函数很可能是内联的,并且条件很可能无论如何都会被优化掉。

请注意,使用'dummy'形参是最接近于实际提供命名构造函数的方法。

这个方法叫做标签调度(我可能搞错了)。具有不同值的枚举类型在编译时只是一种类型,枚举值不能用于重载构造函数。对于enum,它将是一个带有switch语句的构造函数。标签调度相当于编译时的switch语句。每个标记类型指定:这个构造函数将做什么,它将如何尝试获取锁。当你想在编译时做出决定时,你应该使用类型标记,在运行时使用enum来做出决定。

因为,在std::unique_lock<Mutex>中,如果可能永远不需要调用,您不希望强迫 Mutex具有locktry_lock方法。

如果它接受一个enum参数,那么这两个方法都需要存在。

相关文章: