C++ 中的纯/常量函数

发布于 2024-08-31 16:33:42 字数 2026 浏览 5 评论 0原文

我正在考虑在我的 C++ 代码中更多地使用纯/常量函数。 (GCC 中的 pure/const 属性

但是,我很好奇如何我应该严格关注它以及可能发生的情况。

最明显的情况是调试输出(无论何种形式,可以在 cout 上、在某些文件中或在某些自定义调试类中)。我可能会有很多函数,尽管有这种调试输出,但它们不会产生任何副作用。无论是否进行调试输出,这绝对不会对我的应用程序的其余部分产生任何影响。

或者我想到的另一种情况是使用一些 SmartPointer 类,当处于调试模式时,它可能会在全局内存中执行一些额外的操作。如果我在纯/常量函数中使用这样的对象,它确实会产生一些轻微的副作用(从某种意义上说,某些内存可能会有所不同),但不应该有任何真正的副作用(从某种意义上说,行为是在任何不同的方式)。

互斥体和其他东西也类似。我可以想到许多复杂的情况,其中它有一些副作用(从某种意义上说,某些内存会有所不同,甚至可能创建一些线程,进行一些文件系统操作等)但没有计算差异(所有这些副作用很可能被排除在外,我什至更喜欢这样)。

因此,总而言之,我想将严格意义上不是纯/常量的函数标记为纯/常量。一个简单的例子:

int foo(int) __attribute__((const));

int bar(int x) {
   int sum = 0;
   for(int i = 0; i < 100; ++i)
       sum += foo(x);
   return sum;
}

int foo_callcounter = 0;

int main() {
   cout << "bar 42 = " << bar(42) << endl;
   cout << "foo callcounter = " << foo_callcounter << endl;
}

int foo(int x) {
   cout << "DEBUG: foo(" << x << ")" << endl;
   foo_callcounter++;
   return x; // or whatever
}

请注意,函数foo在严格意义上不是const。不过,foo_callcounter 最终是什么并不重要。是否未执行调试语句(如果未调用该函数)也无关紧要。

我期望输出:

DEBUG: foo(42)
bar 42 = 4200
foo callcounter = 1

没有优化:

DEBUG: foo(42) (100 times)
bar 42 = 4200
foo callcounter = 100

两种情况都很好,因为对我的用例来说唯一重要的是 bar(42) 的返回值。

实践中效果如何? 如果我将此类函数标记为 pure/const,是否会破坏任何内容(考虑到代码都是正确的)?


请注意,我知道某些编译器可能根本不支持此属性。 (顺便说一句,我正在这里收集它们。)我也知道如何以代码保持可移植性的方式使用这些属性(通过#defines)。另外,我感兴趣的所有编译器都以某种方式支持它;所以我不关心我的代码是否在编译器上运行得更慢,而编译器则不然。

我还知道,优化后的代码可能看起来会有所不同,具体取决于编译器甚至编译器版本。


这篇 LWN 文章“纯函数和常量函数的含义” 也非常相关,尤其是“秘籍”章。 (感谢 ArtemGr 的提示。)

I'm thinking of using pure/const functions more heavily in my C++ code. (pure/const attribute in GCC)

However, I am curious how strict I should be about it and what could possibly break.

The most obvious case are debug outputs (in whatever form, could be on cout, in some file or in some custom debug class). I probably will have a lot of functions, which don't have any side effects despite this sort of debug output. No matter if the debug output is made or not, this will absolutely have no effect on the rest of my application.

Or another case I'm thinking of is the use of some SmartPointer class which may do some extra stuff in global memory when being in debug mode. If I use such an object in a pure/const function, it does have some slight side effects (in the sense that some memory probably will be different) which should not have any real side effects though (in the sense that the behaviour is in any way different).

Similar also for mutexes and other stuff. I can think of many complex cases where it has some side effects (in the sense of that some memory will be different, maybe even some threads are created, some filesystem manipulation is made, etc) but has no computational difference (all those side effects could very well be left out and I would even prefer that).

So, to summarize, I want to mark functions as pure/const which are not pure/const in a strict sense. An easy example:

int foo(int) __attribute__((const));

int bar(int x) {
   int sum = 0;
   for(int i = 0; i < 100; ++i)
       sum += foo(x);
   return sum;
}

int foo_callcounter = 0;

int main() {
   cout << "bar 42 = " << bar(42) << endl;
   cout << "foo callcounter = " << foo_callcounter << endl;
}

int foo(int x) {
   cout << "DEBUG: foo(" << x << ")" << endl;
   foo_callcounter++;
   return x; // or whatever
}

Note that the function foo is not const in a strict sense. Though, it doesn't matter what foo_callcounter is in the end. It also doesn't matter if the debug statement is not made (in case the function is not called).

I would expect the output:

DEBUG: foo(42)
bar 42 = 4200
foo callcounter = 1

And without optimisation:

DEBUG: foo(42) (100 times)
bar 42 = 4200
foo callcounter = 100

Both cases are totally fine because what only matters for my usecase is the return value of bar(42).

How does it work out in practice? If I mark such functions as pure/const, could it break anything (considering that the code is all correct)?


Note that I know that some compilers might not support this attribute at all. (BTW., I am collecting them here.) I also know how to make use of thes attributes in a way that the code stays portable (via #defines). Also, all compilers which are interesting to me support it in some way; so I don't care about if my code runs slower with compilers which do not.

I also know that the optimised code probably will look different depending on the compiler and even the compiler version.


Very relevant is also this LWN article "Implications of pure and constant functions", especially the "Cheats" chapter. (Thanks ArtemGr for the hint.)

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

ゞ记忆︶ㄣ 2024-09-07 16:33:43

我期望的输出:

我期望输入:

int bar(int x) {
   return foo(x) * 100;
}

你的代码对我来说实际上看起来很奇怪。作为维护者,我认为要么 foo 实际上有副作用,要么更有可能立即将其重写为上述函数。

实际效果如何? 如果我将此类函数标记为 pure/const,是否会破坏任何内容(考虑到代码都是正确的)?

如果代码全部正确,则不会。但你的代码正确的可能性很小。如果您的代码不正确,那么此功能可以掩盖错误:

int foo(int x) {
    globalmutex.lock();
    // complicated calculation code
        return -1;
    // more complicated calculation
    globalmutex.unlock();
    return x;
}

现在给出上面的条:

int main() {
    cout << bar(-1);
}

这以 __attribute__((const)) 终止,但否则会死锁。

这也很大程度上取决于实施。例如:

void f() {
    for(;;) 
    {
        globalmutex.unlock();
        cout << foo(42) << '\n';
        globalmutex.lock();
    }
}

编译器应该将调用 foo(42) 移到哪里?是否允许优化此代码?不是一般情况下!因此,除非循环真的很微不足道,否则您的功能不会带来任何好处。但如果你的循环很简单,你可以轻松地自己优化它。

编辑:由于阿尔伯特要求一个不太明显的情况,所以它来了:
F
或者例如,如果您为 ostream 实现运算符 <<,则可以使用 ostream::sentry 来锁定流缓冲区。假设您在释放之后或锁定之前调用 pure/const f。有人使用这个运算符cout << YourType()f 也使用 cout << “调试信息”。根据您的说法,编译器可以自由地将 f 的调用放入临界区。发生死锁。

I would expect the output:

I would expect the input:

int bar(int x) {
   return foo(x) * 100;
}

Your code actually looks strange for me. As a maintainer I would think that either foo actually has side effects or more likely rewrite it immediately to the above function.

How does it work out in practice? If I mark such functions as pure/const, could it break anything (considering that the code is all correct)?

If the code is all correct then no. But the chances that your code is correct are small. If your code is incorrect then this feature can mask out bugs:

int foo(int x) {
    globalmutex.lock();
    // complicated calculation code
        return -1;
    // more complicated calculation
    globalmutex.unlock();
    return x;
}

Now given the bar from above:

int main() {
    cout << bar(-1);
}

This terminates with __attribute__((const)) but deadlocks otherwise.

It also highly depends on the implementation. For example:

void f() {
    for(;;) 
    {
        globalmutex.unlock();
        cout << foo(42) << '\n';
        globalmutex.lock();
    }
}

Where the compiler should move the call foo(42)? Is it allowed to optimize this code? Not in general! So unless the loop is really trivial you have no benefits of your feature. But if your loop is trivial you can easily optimize it yourself.

EDIT: as Albert requested a less obvious situation, here it comes:
F
or example if you implement operator << for an ostream, you use the ostream::sentry which locks the stream buffer. Suppose you call pure/const f after you released or before you locked it. Someone uses this operator cout << YourType() and f also uses cout << "debug info". According to you the compiler is free to put the invocation of f into the critical section. Deadlock occurs.

撩动你心 2024-09-07 16:33:43

我会检查生成的汇编,看看它们有什么区别。 (我的猜测是,从 C++ 流切换到其他流会产生更多真正的好处,请参阅:http://typethinker.blogspot.com/2010/05/are-c-iostreams-really-slow.html )

I would examine the generated asm to see what difference they make. (My guess would be that switching from C++ streams to something else would yield more of a real benefit, see: http://typethinker.blogspot.com/2010/05/are-c-iostreams-really-slow.html )

§对你不离不弃 2024-09-07 16:33:43

我认为没有人知道这一点(除了 gcc 程序员),仅仅是因为您依赖于未定义和未记录的行为,这些行为可能会因版本而异。但是像这样怎么样:


#ifdef NDEBUG \
    #define safe_pure __attribute__((pure)) \
#else \
    #define safe_pure \
#endif

我知道这并不完全是您想要的,但现在您可以使用 pure 属性而不违反规则。
如果您确实想知道答案,您可以在 gcc 论坛(邮件列表等)中询问,他们应该能够给您确切的答案。
代码含义:当定义NDEBUG(断言宏中使用的符号)时,我们不进行调试,没有副作用,可以使用纯属性。当它被定义时,我们有副作用,所以它不会使用纯属性。

I think nobody knows this (with the exception of gcc programmers), simply because you rely on undefined and undocumented behaviour, which can change from version to version. But how about something like this:


#ifdef NDEBUG \
    #define safe_pure __attribute__((pure)) \
#else \
    #define safe_pure \
#endif

I know it's not exactly what you want, but now you can use the pure attribute without breaking the rules.
If you do want to know the answer, you may ask in the gcc forums (mailing list, whatever), they should be able to give you the exact answer.
Meaning of the code: When NDEBUG (symbol used in assert macros) is defined, we don't debug, have no side effects, can use pure attribute. When it is defined, we have side effects, so it won't use pure attribute.

薄荷港 2024-09-07 16:33:42

我正在考虑在我的 C++ 代码中更多地使用纯/常量函数。

这是一个滑坡。这些属性是非标准,它们的好处主要限于微观优化。

这不是一个好的权衡。相反,编写干净的代码,不要应用此类微观优化,除非您已经仔细分析并且没有办法解决它。或者根本没有。

请注意,原则上这些属性非常好,因为它们明确为编译器和程序员声明了函数的隐含假设。这很好。然而,还有其他方法可以明确类似的假设(包括文档)。但由于这些属性是非标准的,因此它们在正常代码中没有位置。它们应该被限制在性能关键的库中非常明智地使用,在这些库中,作者试图为每个编译器生成最好的代码。也就是说,作者意识到只有 GCC 可以使用这些属性,并对其他编译器做出了不同的选择。

I'm thinking of using pure/const functions more heavily in my C++ code.

That’s a slippery slope. These attributes are non-standard and their benefit is restricted mostly to micro-optimizations.

That’s not a good trade-off. Write clean code instead, don’t apply such micro-optimizations unless you’ve profiled carefully and there’s no way around it. Or not at all.

Notice that in principle these attributes are quite nice because they state implied assumptions of the functions explicitly for both the compiler and the programmer. That’s good. However, there are other methods of making similar assumptions explicit (including documentation). But since these attributes are non-standard, they have no place in normal code. They should be restricted to very judicious use in performance-critical libraries where the author tries to emit best code for every compiler. That is, the writer is aware of the fact that only GCC can use these attributes, and has made different choices for other compilers.

芯好空 2024-09-07 16:33:42

你肯定会破坏代码的可移植性。为什么你想要实现你自己的智能指针——除了学习经验之外? (近)标准库中没有足够的可用资源吗?

You could definitely break the portability of your code. And why would you want to implement your own smart pointer - learning experience apart? Aren't there enough of them available for you in (near) standard libraries?

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文