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

时间:2015-08-12 06:22:56

标签: c++ c++11

在C ++ 11中,重载std::unique_lock constructor以接受类型标记defer_lock_ttry_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 );

这些是空类(类型标签)defined as follows

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

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

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 ++代码?

6 个答案:

答案 0 :(得分:11)

标签调度

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

标记的原因是用于标记的类型因此不相关,并且在重载解析期间不会发生冲突。类型(而不是枚举的值)用于解析重载的函数。此外,标签可用于解决原本不明确的呼叫;在这种情况下,标签通常基于某些类型的特征。

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

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

Boost.org有关于标签调度技术的文章。它的使用历史似乎可以追溯到at least as far as the SGI STL

为什么要使用它?

  

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

在重载解析和可能的实现过程中,类型将比枚举更强大和灵活;请记住,枚举最初是无遮盖的,并且限制了它们的使用方式(与标签形成对比)。

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

  • 可以使用哪个构造函数进行编译时决策,而不是运行时。
  • 禁止更多" hacky"代码,其中整数被强制转换为枚举类型,其值不符合要求 - 需要设计决策来处理此问题,然后执行代码以满足任何结果异常或错误。
  • 请注意,shared_locklock_guard也会使用这些标记,但在lock_guard的情况下,只使用adopt_lock。枚举会引入更多潜在的错误条件。

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

  

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

这基本上是一个设计决定。两者都可以而且应该用于解决他们解决的问题。我已经使用标签来"路由"数据和类型到正确的功能;特别是当实现在编译时不兼容并且存在任何重载决策时。

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

如果使用得当,它是一种强大的技术,不应忽视。如果使用枚举,请使用旧的未作用域的枚举枚举。

答案 1 :(得分:0)

使用这些标记可以利用该语言的类型系统。这与模板元编程密切相关。简单来说,使用这些标记允许调度决定在编译时静态地调用哪个构造函数。这为编译器优化留出了空间,提高了运行时效率,并使std::unique_lock的模板元编程更容易。这是可能的,因为标签具有不同的静态类型。使用enum时,无法完成此操作,因为在编译时无法预见enum的值。请注意,使用标记进行区分是一种常见的模板元编程技术。只需查看标准库使用的iterator tags

答案 2 :(得分:-1)

关键是,如果您想使用enum添加其他功能,则应编辑enum,然后重建所有使用您的功能和enum的项目。此外,还有一个函数将枚举作为参数并使用switch或其他东西。这会将多余的代码带入您的应用程序。

否则,如果您将重载函数与标记一起使用,则可以轻松添加另一个标记并添加另一个重载函数,而不会触及旧标记。这更具反向兼容性。

答案 3 :(得分:-1)

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

在许多实现中,锁的获取和释放频率非常高,设计人员可能会考虑分支预测和隐含的内存同步事件,这可能是一个重要问题。

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

请注意,使用'dummy'参数是与实际提供命名构造函数最接近的模拟。

答案 4 :(得分:-1)

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

答案 5 :(得分:-1)

因为,在std::unique_lock<Mutex>中,您不希望强制 Mutex拥有locktry_lock方法可能永远不需要被召唤。

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