GCC C ++编译器通过function attributes提供一系列扩展,例如:
int square(int) __attribute__((const));
特别是两个属性const
和pure
,允许您声明函数的评估没有副作用,并且仅依赖于其参数(const
),或仅依赖于它参数和全局变量(pure
)。这允许消除常见的子表达式,这可能会导致调用此类函数的次数少于在代码中编写的次数。
我的问题是,这是否可以安全,正确和合理地用于虚拟成员函数:
struct Foo
{
virtual int square(int) __attribute__((pure)); // does that make sense?
};
这有任何明智的语义吗?它是否允许?还是只是被忽略了?我担心在GCC文档中找不到答案。
这个问题的原因是存在一系列编译器选项-Wsuggest-attribute
,这些选项使GCC产生关于可以放置这些属性以改进代码的位置的建议。然而,它似乎最终甚至为虚拟功能提出了这些建议,我想知道是否应该认真对待这些建议。
答案 0 :(得分:4)
GCC允许并接受。它通常被忽略 not (你知道这是因为GCC总是输出warning: attribute ignored
,当它完全忽略一个属性时,它不在这里)。但也请阅读最后一段。
是否有意义是另一个问题。虚函数可以重载,您可以在没有属性的情况下重载它。这将打开下一个问题:此是否合法?
可以预期具有不同属性的函数具有不同的签名(例如使用const
限定符或不同的异常规范),但事实并非如此。 GCC在这方面将它们视为完全相同。您可以通过从Bar
派生Foo
并实现非const的成员函数来验证这一点。然后
decltype(&Bar::square) f1 = &Foo::square;
decltype(&Foo::square) f2 = &Bar::square;
将在第二行中给出编译时错误,但不会在第一行中出现,正如您所期望的那样。如果签名不同(尝试使用const限定函数,而不是使用属性!),第一个行就会出错。
最后,它是否安全,是否有意义?它始终是安全的,编译器必须确保它是安全的。它在语义上有意义,在限度内。
从语义的角度来看,声明函数const
或pure
是“正确的”,如果它真的是这样的话。但是,如果你向界面的用户做出“承诺”,这可能不是真的,这有点尴尬。有人可能会调用此函数,在派生类中,所有外观都是const
,而这不是真的。编译器必须确保它仍然有效,但用户对性能的期望可能与现实不同。
将函数标记为const
或pure
可能允许编译器更好地进行优化。现在,使用虚函数,这有点困难,因为对象可能是派生类型,而这不是真的!
这必然意味着编译器必须忽略优化属性,除非可以静态解析虚拟调用。情况可能仍然如此,但一般情况下并非如此。
答案 1 :(得分:3)
在您链接到的文档中,在const属性的描述下有这个注释:
请注意,具有指针参数并检查指向的数据的函数不得声明为const。
我说这包括成员函数,因为它们有一个隐式指针参数(至少虚函数需要检查它才能到达vtable,不是吗?)。
他们似乎在这个帖子中得到了类似的结论:http://gcc.gnu.org/ml/gcc/2011-02/msg00460.html
答案 2 :(得分:3)
第一个问题是这些属性是否对虚拟方法具有有效的语义。在我看来他们这样做。我希望如果一个虚函数标记为纯,你会承诺编译器所有实现只依赖于它们的参数和全局内存中的数据(并且不要改变它),其中全局内存中的数据也包括内容物体。如果虚函数被标记为const,这意味着它只能依赖于它的参数,甚至不允许检查对象的内容。编译器必须强制所有重写的虚拟方法声明属性至少与其父级一样强。
接下来的问题是GCC是否使用这些属性进行优化。在下面的测试程序中,您可以看到版本4.6.3没有(尝试使用-O3编译汇编程序,您将看到循环已展开)。
struct A {
virtual int const_f(int x) __attribute__((const)) = 0;
};
int do_stuff(A *a) {
int b = 0;
for (int i=0; i<10; i++) {
b += a->const_f(0);
}
return b;
}
即使在编译时已知类型的以下程序中,编译器也不会优化循环。
struct A {
virtual int const_f(int x) __attribute__((const)) = 0;
};
struct B : public A {
int const_f(int x) __attribute__((const));
};
int do_stuff(B *b) {
int c = 0;
for (int i=0; i<10; i++) {
c += b->const_f(0);
}
return c;
}
从A中删除继承(从而使方法非虚拟)允许编译器进行预期的优化。
没有关于这些属性的标准或文档,因此我们可以获得的最佳参考是实现。由于它们目前没有任何效果,我建议避免将它们用于虚拟方法,以防将来行为意外地发生变化。
答案 3 :(得分:1)
G ++ 4.8.1 似乎尊重虚拟成员函数上的pure
和const
函数属性。
给出以下源代码:
struct Base {
void w();
void x() __attribute__ ((const));
virtual void y();
virtual void z() __attribute__ ((const));
};
struct Derived : public Base {
void w() __attribute__ ((const));
void x();
virtual void y() __attribute__ ((const));
virtual void z();
};
void example() {
Base b, *pb;
Derived d, *pd;
b.w(); // called
b.x(); // not called
b.y(); // called
b.z(); // not called
pb->w(); // called
pb->x(); // not called
pb->y(); // called
pb->z(); // called
d.w(); // not called
d.x(); // called
d.y(); // not called
d.z(); // called
pd->w(); // not called
pd->x(); // called
pd->y(); // called
pd->z(); // called
}
...编译器生成以下(摘录)汇编代码:
void example() {
Base b, *pb;
Derived d, *pd;
b.w(); // called
1c: e8 00 00 00 00 callq 21 <_Z7examplev+0x21>
b.x(); // not called
b.y(); // called
21: 48 89 e7 mov %rsp,%rdi
24: e8 00 00 00 00 callq 29 <_Z7examplev+0x29>
b.z(); // not called
pb->w(); // called
29: 48 89 df mov %rbx,%rdi
2c: e8 00 00 00 00 callq 31 <_Z7examplev+0x31>
pb->x(); // not called
pb->y(); // called
31: 48 8b 2b mov (%rbx),%rbp
34: 48 89 df mov %rbx,%rdi
37: ff 55 00 callq *0x0(%rbp)
pb->z(); // called
3a: 48 89 df mov %rbx,%rdi
3d: ff 55 08 callq *0x8(%rbp)
d.w(); // not called
d.x(); // called
40: 48 8d 7c 24 10 lea 0x10(%rsp),%rdi
45: e8 00 00 00 00 callq 4a <_Z7examplev+0x4a>
d.y(); // not called
d.z(); // called
4a: 48 8d 7c 24 10 lea 0x10(%rsp),%rdi
4f: e8 00 00 00 00 callq 54 <_Z7examplev+0x54>
pd->w(); // not called
pd->x(); // called
54: 48 89 df mov %rbx,%rdi
57: e8 00 00 00 00 callq 5c <_Z7examplev+0x5c>
pd->y(); // called
5c: 48 8b 2b mov (%rbx),%rbp
5f: 48 89 df mov %rbx,%rdi
62: ff 55 00 callq *0x0(%rbp)
pd->z(); // called
65: 48 89 df mov %rbx,%rdi
68: ff 55 08 callq *0x8(%rbp)
}