为什么C++标准要求`Clock::now`函数是静态的?

14

在C++11标准中,C++有一些计时设施。其中之一是时钟的标准接口,它基本上允许在调用时获取时钟now函数的时间。

到此为止一切都很好,但我没有看到需要now成为静态函数的原因。在托管系统上,标准时钟可以仅通过系统调用或读取处理器计数器等来实现。然而,这限制了需要维护一些状态的自定义时钟的实现。使用此接口,要么无法实现某些时钟,要么必须使用全局状态。

我遇到的一个问题基本上是将本地时钟与从NTP服务器获取的时间同步。代码看起来就像这样:

class sntp_clock
{
public:
    sntp_clock() 
        : local_time_at_ctor(read_some_cpu_counter())
        , sntp_time_at_ctor(read_sntp_time()) {}
 
    sntp_time_t now() const {
        return sntp_time_at_ctor + (read_some_cpu_counter() - local_time_at_ctor);
    }

    /* required types etc */

private:
    local_time_t local_time_at_ctor;
    sntp_time_t sntp_time_at_ctor;
};

由于我不能使now静态而不使状态也变为静态,因此这个时钟不满足C++标准中Clock的要求。但是每个NTP服务器都将有单独的状态。

另外,出于效率原因,我可能不想在时钟实例存在时启动CPU计数器,但是,由于now是静态的,我无法知道何时开始计时,何时停止计时。

我的问题是为什么时钟会有静态now的要求?

注意:当前标准草案要求now是静态的:http://eel.is/c++draft/time.clock.req#tab:time.clock

Boost.Chrono文档也有同样的要求:https://www.boost.org/doc/libs/1_63_0/doc/html/chrono/reference.html#chrono.reference.cpp0x.clock


7
@tadman,我认为问题在于询问委员会为什么选择以那种方式编写标准。 - François Andrieux
3
@tadman,我想知道这个决定的理由。从技术上讲,所有的时钟都有一些状态,只是操作系统会为你维护这个状态。在我们的嵌入式操作系统中,我必须实现这些时钟,并希望使它们与C++标准兼容。 - Fatih BAKIR
4
@RobertHarvey,我可以确定Howard Hinnant在这里。此外,如果我们得到答案,我希望把它放在这里,以便其他可能会有同样疑问的人可以看到。 - Fatih BAKIR
2
@FatihBAKIR:“实际上,我想知道在未来的标准中是否可以去除限制。” 不,不可能。所有编写期望具有静态“now”函数的“Clock”的模板代码都将期望它具有静态“now”函数。因此,在非静态函数上会出现错误。 - Nicol Bolas
2
@FatihBAKIR 记住,如果您想做出贡献,可以通过程序提交提案。最坏的情况是,您的提案被拒绝,但您也会得到对您问题的答案。 - François Andrieux
显示剩余20条评论
1个回答

28

这个决定是由理论和实际问题驱动的。

理论问题

有一个笑话说,戴着手表的人总是知道现在几点了,但戴着两只手表的人永远不知道。这个笑话里有一点点真实性,它确实影响了这个决定。如果一个应用程序需要知道两个或更多地方的当前时间,并且时钟是有状态的,那么就存在一个问题:如何确保所有代码部分都使用相同的时钟实例来保证所有代码部分都处理“当前时间”的相同定义。

通过使时钟无状态,但允许使用不同类型的多个时钟,类型系统可以帮助程序员确保程序在程序的不同位置使用相同的当前时间定义。而对于那些需要多个时间定义的情况,也可以像不同的类型一样使用。

实际问题

作为更实际的考虑,chrono::clock代码的第一个客户端是chrono本身。我们必须吃自己的狗粮。以condition_variable::wait_until的实现为例:

https://github.com/llvm-mirror/libcxx/blob/master/include/__mutex_base#L377-L385

template <class _Clock, class _Duration>
cv_status
condition_variable::wait_until(unique_lock<mutex>& __lk,
                               const chrono::time_point<_Clock, _Duration>& __t)
{
    using namespace chrono;
    wait_for(__lk, __t - _Clock::now());
    return _Clock::now() < __t ? cv_status::no_timeout : cv_status::timeout;
}

这里有一个函数,它接收一个通用的time_point参数,并且算法需要找到与该time_point相关联的当前时间。通过将Clock类型打包到time_point类型中并具有static now(),代码编写非常简洁,并且具有非常干净的接口。然而,它足够通用,以便此代码将适用于任何用户编写的自定义时钟:不仅限于std指定的时钟。
如果时钟具有状态,则:
1. condition_variable::wait_until无法获取当前时间,或者 2. 客户端还必须传递time_point所测量的时钟。
以上两种选择都不可取。
请注意,condition_variable::wait_until不是特例,而只是许多这类算法中的一个示例。事实上,我假设不仅标准实现人员会编写这样的算法,而且普通公众也会编写这样的算法。以下是后者的一个示例:

https://dev59.com/P1sW5IYBdhLWcg3wM0yF#35293183


是的,我遇到过人们想要有状态时钟的情况。本问题的 OP 就提供了这样的例子。但是由于还有“有状态时钟”可以给出静态状态的选项,如果需要其他状态,则使用不同类型;并且由于上述无状态时钟的优点,选择无状态时钟设计更具优势。

更新1

我对客户的说法进行了更多思考:

那么我的有状态时钟不是好代码吗?

我认为,只要人们意识到其限制,有状态时钟就可以使用。不幸的是,由于标准中的一个小错误,这个问题变得比必要的复杂。

从实际角度来看,有状态时钟只有一件事情做不到,而无状态时钟可以做到,这涉及到上面标题为实用性的部分。

您不能实例化需要模板参数为 ClockTM 的算法(std 或其他)。

例如,如果您有一个基于有状态时钟的time_point,则无法使用该 time_point 调用 condition_variable::wait_until。如果您不想这样做,那并不意味着您的有状态时钟不好。如果您的有状态时钟能够达到您的目的,那就很好

在 C++20 中,甚至有一个时钟的示例,它不符合 C++11-17 时钟要求的所有条件:

struct local_t {};

是的,那(有点像)是一只时钟。但它几乎什么也做不了。它用于创建一组没有关联now()time_point

template<class Duration>
    using local_time  = time_point<local_t, Duration>;

这在区分与尚未指定时区相关的时间点(考虑类型安全)时非常有用。
如果创建一个没有static now()的时钟对于标准来说是可以的,那为什么对你来说不行呢?我能想到的唯一原因就是上面提到的标准中的“小错误”。
C++20规范中的27.6 [time.point]针对template<class Clock, class Duration> class time_point提到:
1 Clock应该满足Cpp17Clock要求(27.3),或者是类型local_t
我现在认为这太过严格了。程序员应该能够使用具有状态的时钟实例化time_point。他们只是不能使用那个time_point调用condition_variable::wait_until(等等)。但是他们仍然可以获得使用那个time_point和由其差异产生的duration的所有代数优势。
除了标准规定之外,没有什么好的理由限制这一点。而local_t的存在就充分证明了这一点。

1 Clock应该满足Cpp17Clock要求(27.3),或者是类型local_t,如果实例化默认模板参数Duration,则应该有嵌套类型duration

更新2

现在有一篇跟踪此问题的论文

更新3

拟议的更改现在已经包含在C++20标准后的工作草案中:

http://eel.is/c++draft/time.point.general

http://eel.is/c++draft/thread.req.paramname

非常感谢Alexey Dmitriev推动这项工作。

7
很棒你看到并回复了这条信息。我刚阅读N2661,看看是否有涉及的内容,并得出了相同的结论。感谢你的时间。;) - NathanOliver
3
我希望我当初能有先见之明(和足够的时间)把这个推理过程写进N2661。当时我非常忙,几乎没有时间记录所有必要的内容。 - Howard Hinnant
你没有提到这一点,我认为静态函数的一个被低估的特性是它允许将接口泛化到有状态对象,并且相同的接口可以工作,例如 template<class FancyClock> void f(FancyClock c){... c.now();}(对于静态或成员函数now()都适用,c可以有状态或无状态)。像这样的东西是否与选择(或更确切地说,参考实现)库函数作为静态有关? - alfC
这是一个很好的观点。我知道这种语言特性。但是我没有一个具体的动机使用这种语言特性在时间点或时钟的通用函数中。我看过为有状态时钟编写的代码,可能由于这种语言特性,可以适用于无状态时钟。但这样的代码并不真正通用。我同意这是一种被低估的语言特性(能够使用obj.语法调用静态成员函数)。 - Howard Hinnant

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