我应该在我的函数中使用静态变量来防止重新计算值吗?

Posted

技术标签:

【中文标题】我应该在我的函数中使用静态变量来防止重新计算值吗?【英文标题】:Should I use static variables in my functions to prevent recomputing values? 【发布时间】:2019-10-28 00:12:36 【问题描述】:

我已经编写 C++ 代码有一段时间了,但我一直在想一些事情,但一直没有找到明确的答案。

我的观点如下:假设我有一个函数(可以是方法,可以是static,但不一定),并且该函数使用一些“重”对象(例如字符串在编译时无法轻松确定,但在整个执行过程中保持不变)。我实际遇到的一个例子如下:

/* Returns an endpoint for an API
 * Based on the main API URL (getApiUrl())
 */
virtual QString getEndPointUrl() const override

    QString baseUrl = getApiUrl();
    QString endpointUrl = QString("%1/%2").arg(baseUrl, "endpoint");
    return endpointUrl;

这当然只是一个例子(我知道QStrings 有自己花哨的 Qt 内存管理功能,但让我们承认我们正在处理基本对象)。

执行以下操作是个好主意吗?

virtual QString getEndPointUrl() const override

    /* We determine baseUrl only once */
    static const QString baseUrl = getApiUrl();
    /* We compute endpointUrl only once */
    static const QString endpointUrl = QString("%1/%2").arg(baseUrl, "endpoint");
    return endpointUrl;

您可能已经猜到了,这里的想法是不要在每次执行 getEndPointUrl 时确定 URL。

我发现的唯一缺点是内存使用率较高(因为对象是在第一次调用函数时构建的,并且仅在程序结束时才销毁)。

另一件事是,拥有无状态函数被认为是一种“更好”的做法,但我真的不认为这种行为可以被称为“状态”。

编辑:我只是想指出我计算的值在函数之外是没有意义的,否则它们可能是封闭类的字段或其他任何东西,但它们从未在其他任何地方使用。

你有什么想法?

【问题讨论】:

如果不是静态函数,我会计算构造函数中的值。也许它稍后会在另一个函数中使用。 我同意这不符合“状态”的条件,我也认为这完全没问题,但我感觉这将作为“主要基于意见”而关闭。 你的虚函数是在一个类中声明的,不是吗?在调用函数的任何地方都使用 endpointUrl。将 endpointUrl 存储在您的虚函数所在的类中是有意义的。我不会使用静态变量。在程序结束 imo 之前保留它是没有意义的。 @MFnx 我认为使函数虚拟化的目的是能够在派生类中覆盖它(并可能使结果更加动态)。你不能用成员变量来做到这一点。 @molbdnilo - 你也不能用静态来做到这一点。重写虚函数是关于根据对象的实际类型改变调用该函数的行为。使用类的成员变量不一定与此相关。 【参考方案1】:

这取决于您的上下文。正如您正确指出的那样,您正在用内存使用来换取速度。那么在您的上下文中,什么更重要(如果有的话)?你在低延迟程序的热门路径上吗?你在低内存设备上吗?

【讨论】:

实际上,这更像是一个理论上的“如果我在一个低延迟程序中会怎样”的问题 @ThomasKowalski 那就去吧!不这样做将是过早的悲观。【参考方案2】:

是的,绝对!

与以往一样,需要权衡取舍,您已经确定了这一点。

但这是完全正常且明智的做法。如果您不需要每次都计算这些值,那就不要。

在这种特殊情况下,我可能会将这些项目设为封装类的静态成员,除非您强烈需要延迟实例化(也许函数不会在每次运行时调用,并且您认为这些初始化过于“繁重”在您不需要它们时执行)。

事实上,这会使整个函数 getEndPointUrl() 过时。让它成为一个公共成员常量!您认为常量“未在其他任何地方使用”的理由有点循环论证;无论在哪里使用 getEndPointUrl(),您都可以使用这些数据。

【讨论】:

为什么是静态成员而不是普通成员?在创建另一个类实例时避免重构? @MFnx 为什么非静态成员而不是静态成员?它不包含任何特定于实例的状态,是吗? 只是为了确保它不会永远存在......除非它有真正的好处(我们不知道),我更喜欢 const(私人)成员。 之所以将它作为一个函数而不是一个成员是因为我的类派生自一个更通用的类,并且每个“姐妹”类都有不同的getEndPointUrl 实现。 is 是否对所有实例都通用。 @MFnx 我认为这是对会员的滥用。 const 成员的初始化不是来自构造参数通常是错误的 :)【参考方案3】:

这似乎是一个可行的选择。我对您的示例有一个建议的更改,那就是在第二个静态的初始化程序中调用getApiUrl(),使其成为唯一的初始化程序......因此:

static const QString endpointUrl = QString("%1/%2").arg(getApiUrl(), "endpoint");

在您的程序的整个生命周期中,这减少了一个对象。

使用静态缓存时有许多问题:

    您无法控制该对象的生命周期何时结束。这可能是也可能不是问题,具体取决于该对象是否需要指向其他对象的指针/引用才能正确清理自身。 我不相信静态对象解构顺序有任何保证...可能有必要进一步将东西包装在 shared_ptr 中,以确保不会从静态对象下抽出资源。 缓存通常提供一种方法来清除其存储的值并重新填充。不存在这样的静态机制,尤其是const statics。

编辑:有传言说线程安全不是问题。

但如果这些问题都不适用于您的特定用例,那么请务必使用static

编辑:重要的评论回复:

我不能强烈建议不要依赖静态对象销毁顺序。

想象一下改变你的程序,使你的资源加载系统现在在你的日志系统之前启动。您设置一个断点并单步执行新代码,然后您会发现一切都很好。您结束调试会话,运行单元测试,它们都通过了。检查源代码,夜间集成测试失败...如果你幸运的话。否则,您的程序会在客户面前退出时开始崩溃。

事实证明,您的资源系统尝试在关闭时记录某些内容。繁荣。但是,嘿......一切仍然运行良好!何必费心修呢?

哦,事实证明,您的资源系统尝试记录的内容是错误情况,只会成为问题......对于您最大的客户。

哎哟。

不要成为那个人。不要依赖静态析构函数的顺序。

(好吧,现在想象一下对象构造/销毁的顺序不是确定性的,因为其中一些具有静态对象的函数是从不同的线程调用的。做噩梦了吗?)

【讨论】:

“我相信静态初始化也存在一些线程安全问题” static init 是线程安全的。 静态对象的破坏顺序是明确定义的——与它们的构造顺序相反。当然,你真的不知道他们的施工顺序是什么,所以...... @molbdnilo 对函数静态来说是这样吗?似乎实际上很难完成,因为直到运行时您才知道构造顺序。 嗯,好像是这样。所以有一种运行时排序。魔术。【参考方案4】:

对于您的具体示例

在您的示例中,缓存基本上不会产生性能差异,因此使用 static 可能是唯一值得努力的方法。

如果您的计算实际上很昂贵,以下是关于这种缓存方式的一些想法:

一般情况

优点:

它是自动线程安全的 非常容易使用

缺点:

维护
    实际上是一个单例with all of it's problems 如果突然getUrl() 需要返回不同的值,可能会成为一个令人惊讶的错误 不适合单元测试 等 很多人不知道 static 关键字的作用 (如果您多次链接静态库,您将获得该变量的多个实例。可能是ODR violation)
功能 也许不希望第一次调用函数花费更长的时间 结构 如果缓存对象引用堆栈上的某些内容,则可能在释放后使用。

^(请注意,其中很多不适用于特定情况)

替代方案

按优先顺序:

    设置/更改 getUrl() 的值时缓存该值。 缓存对象中的值 使用你 inject 的缓存(一些 map<string, shared_ptr<void>> 或其他东西)(为此矫枉过正)

【讨论】:

感谢您的详细回答。实际上,您引用的其中一个缺点(getUrl 需要返回不同的值)在这个确切的项目中发生在我身上,而且调试起来非常困难。我想我不会犯两次错误,但仍然需要考虑到这一点。

以上是关于我应该在我的函数中使用静态变量来防止重新计算值吗?的主要内容,如果未能解决你的问题,请参考以下文章

我应该使用训练数据集的函数来处理训练数据集和测试数据集的缺失值吗

在 Excel 中应用自动筛选时防止重新计算函数

我可以从当前线程中保存一个值吗?

在c#中“动态”更改静态变量

我如何使用这种类型的函数来总结 3 天的销售交易并计算 3 天的销售交易并在我的主要逻辑中使用它

如何防止我的makefile由于静态库而重新链接