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

fnx2tebb  于 2024-01-09  发布在  其他
关注(0)|答案(6)|浏览(133)

在C++11中,std::unique_lock constructor被重载以接受类型标记defer_lock_ttry_to_lock_tadopt_lock_t

  1. unique_lock( mutex_type& m, std::defer_lock_t t );
  2. unique_lock( mutex_type& m, std::try_to_lock_t t );
  3. unique_lock( mutex_type& m, std::adopt_lock_t t );

字符串
以下是空类(类型标记)defined as follows

  1. struct defer_lock_t { };
  2. struct try_to_lock_t { };
  3. struct adopt_lock_t { };


这允许用户通过传递这些类中的pre-defined instances之一来消除三个构造函数之间的歧义:

  1. constexpr std::defer_lock_t defer_lock {};
  2. constexpr std::try_to_lock_t try_to_lock {};
  3. constexpr std::adopt_lock_t adopt_lock {};


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

  • 更容易实现
  • 不改变语法
  • 允许在运行时更改参数(尽管在这种情况下不是很有用)。
  • (可能)可以由编译器内联,而不会影响性能
    **为什么标准库使用类型标记,而不是enum,来消除这些构造函数的歧义?**也许更重要的是,我在编写自己的C++代码时,是否也应该在这种情况下使用类型标记?
mxg2im7a

mxg2im7a1#

标签分发

这是一种被称为标记调度的技术。它允许在给定客户端所需的行为的情况下调用适当的构造函数。
使用标记的原因是,用于标记的类型因此是不相关的,并且在重载解析期间不会冲突。类型(而不是枚举情况下的值)用于解析重载函数。此外,标记可以用于解析否则会有二义性的调用;在这种情况下,标记通常基于某些类型trait。
使用模板分派标记意味着只需要实现给定构造所需使用的代码。
标签调度允许更容易的阅读代码(至少在我看来是这样)和更简单的库代码;构造函数不会有switch语句,并且在执行构造函数本身之前,可以基于这些参数在初始化器列表中建立不变量。当然,你的里程可能会有所不同,但这是我使用标签的一般经验。
Boost.org有一篇关于标签调度技术的文章,它的使用历史似乎可以追溯到at least as far as the SGI STL

为什么要使用?

为什么标准库使用类型标记而不是枚举来消除这些构造函数的歧义?
在重载解析和可能的实现中使用时,类型比枚举更强大和灵活;请记住,枚举最初没有作用域,并且在如何使用它们方面受到限制(与标记相反)。
标签的其他值得注意的原因;

  • 编译时可以决定使用哪个构造函数,而不是运行时。
  • 不允许更多的“黑客”代码,其中整数被转换为枚举类型,其值不适合-需要做出设计决策来处理这种情况,然后实现代码以满足任何结果异常或错误。
  • 请记住,shared_locklock_guard也使用这些标记,但在lock_guard的情况下,只使用adopt_lock。枚举将引入更多潜在的错误条件。

我认为优先级和历史在这里也起着作用。考虑到标准库和其他地方的广泛使用;它不太可能改变库中实现的情况,例如原始示例。
也许更重要的是,在编写自己的C++代码时,我是否也应该在这种情况下使用类型标记?
这本质上是一个设计决策。两者都可以而且应该被用来针对它们所解决的问题。我已经使用标记来“路由”数据和类型到正确的函数;特别是当实现在编译时不兼容时,以及如果有任何重载解决方案在起作用时。
标准库std::advance经常被作为一个例子,说明如何使用标记调度来实现和优化基于所用类型的特性(或特征)的算法(在这种情况下,迭代器是随机访问迭代器)。
如果使用得当,它是一种强大的技术,不应该被忽视。如果使用枚举,最好使用新的作用域枚举而不是旧的无作用域枚举。

展开查看全部
uz75evzq

uz75evzq2#

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

t1rydlwq

t1rydlwq3#

重点是,如果你想使用enum添加另一个函数,你应该编辑你的enum,然后重建所有项目,这些项目使用你的函数和enum。此外,将有一个函数以枚举为参数,并使用switch或其他东西。这将给你的应用程序带来多余的代码。
否则,如果你使用带标记的重载函数,你可以很容易地添加另一个标记,并添加另一个重载函数,而不会触及旧的标记。这是更向后兼容的。

bd1hkmkf

bd1hkmkf4#

我怀疑这是优化。注意,使用类型(按原样)是在编译时选择正确的版本。正如你指出的,使用enum是(潜在地)在运行时在某些条件语句(可能是switch)中选择的。
在许多实现中,锁以极高的频率被获取和释放,并且设计者可能认为分支预测和隐含的内存同步事件可能是一个重要的问题。
我的论点中的缺陷(你也指出了)是构造函数很可能是内联的,而且无论如何条件都可能被优化掉。
请注意,使用“dummy”参数是与实际提供命名构造函数最接近的模拟。

mfuanj7w

mfuanj7w5#

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

yzxexxkh

yzxexxkh6#

因为,在std::unique_lock<Mutex>中,如果Mutex可能永远不需要被调用,您不希望 * 强制 * Mutex拥有locktry_lock方法。
如果它接受enum参数,那么这两个方法都需要存在。

相关问题