我正在考虑在我的C ++代码中更多地使用pure / const函数。 (pure/const attribute in GCC)
但是,我很好奇我应该对它有多严格,哪些可能会破坏。
最明显的情况是调试输出(无论采用何种形式,都可以在cout,某些文件或某些自定义调试类中)。我可能会有很多功能,尽管有这种调试输出,但它们没有任何副作用。无论是否进行调试输出,这绝对不会影响我的应用程序的其余部分。
或者我想到的另一种情况是使用一些SmartPointer类,它可能在处于调试模式时在全局内存中做一些额外的事情。如果我在pure / const函数中使用这样的对象,它确实会有一些轻微的副作用(在某种意义上说某些内存可能会有所不同),但它们不应该有任何真正的副作用(从某种意义上说,行为是在任何方式不同)。
类似于互斥体和其他东西。我可以想到许多复杂的情况,它有一些副作用(从某种意义上说,某些内存会有所不同,甚至可能会创建一些线程,进行某些文件系统操作等),但没有计算差异(所有这些副作用)很可能会被遗漏,我甚至更愿意这样做。
因此,总而言之,我想将函数标记为纯/ const,严格意义上不是纯/ const。一个简单的例子:
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,它是否会破坏任何东西(考虑到代码都是正确的)?
请注意,我知道某些编译器可能根本不支持此属性。 (顺便说一下,我正在收集它们here。)我也知道如何以代码保持可移植的方式使用thes属性(通过#defines)。此外,我感兴趣的所有编译器都以某种方式支持它;所以我不关心我的代码是否运行得慢,编译器没有。
我也知道优化的代码可能会因编译器甚至编译器版本而有所不同。
非常相关也是this LWN article "Implications of pure and constant functions",特别是“秘籍”一章。 (感谢ArtemGr提示。)
答案 0 :(得分:17)
我正在考虑在我的C ++代码中更多地使用pure / const函数。
那是一个滑坡。这些属性非标准,其优势主要限于微优化。
这不是一个很好的权衡。编写干净的代码,不要应用这样的微优化,除非你仔细分析并且没有办法解决它。或者完全没有。
请注意,原则上这些属性非常好,因为它们明确规定了编译器和程序员对函数的隐含假设。那是好。但是,还有其他方法可以明确地做出类似的假设(包括文档)。但由于这些属性是非标准的,因此它们在普通代码中具有无处理。它们应该限制在性能关键库中非常明智地使用,其中作者试图为每个编译器发出最佳代码。也就是说,作者意识到只有GCC可以使用这些属性,并为其他编译器做出了不同的选择。
答案 1 :(得分:3)
您肯定会破坏代码的可移植性。为什么你想要实现自己的智能指针 - 学习经验分开?在(附近)标准库中是否有足够的可用它们?
答案 2 :(得分:1)
我希望输出:
我希望输入:
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)
?是否允许优化此代码?不一般!因此,除非循环非常简单,否则您的功能无益处。但是如果你的循环很简单,你可以自己轻松优化它。
operator <<
,则使用锁定流缓冲区的ostream :: sentry。假设您在发布后调用pure / const f
,或者在之前将调用为。有人使用此运算符cout << YourType()
,f
也使用cout << "debug info"
。据你所知,编译器可以自由地将f
的调用放入临界区。发生死锁。
答案 3 :(得分:0)
我认为没有人知道这一点(gcc程序员除外),只是因为你依赖于未定义和未记录的行为,这些行为可能会因版本而异。但是这样的事情怎么样:
#ifdef NDEBUG \
#define safe_pure __attribute__((pure)) \
#else \
#define safe_pure \
#endif
我知道这不是你想要的,但现在你可以使用纯属性而不违反规则。
如果你想知道答案,你可以在gcc论坛(邮件列表,无论如何)中询问,他们应该能够给你准确的答案。
代码的含义:当定义NDEBUG(断言宏中使用的符号)时,我们不调试,没有副作用,可以使用纯属性。当它被定义时,我们有副作用,所以它不会使用纯属性。
答案 4 :(得分:0)
我会检查生成的asm,看看它们有什么不同。 (我的猜测是从C ++流转换到其他东西会带来更多真正的好处,请参阅:http://typethinker.blogspot.com/2010/05/are-c-iostreams-really-slow.html)