13

GCC C++ 编译器通过函数属性提供了一系列扩展,例如:

int square(int) __attribute__((const));

特别是两个属性constpure,允许您声明函数的评估没有副作用,并且仅取决于其参数(const),或仅取决于其参数和全局变量(pure)。这允许消除公共子表达式,这可能会导致调用此类函数的次数少于在代码中编写的次数。

我的问题是这是否可以安全、正确和合理地用于虚拟成员函数:

struct Foo
{
    virtual int square(int) __attribute__((pure));   // does that make sense?
};

这有任何合理的语义吗?是否允许?或者它只是被忽略了?恐怕我在 GCC 文档中找不到这个问题的答案。

这个问题的原因是有一系列编译器选项-Wsuggest-attribute使 GCC 产生建议,这些属性可以放置在何处以改进代码。然而,似乎最终甚至对虚拟功能也提出了这些建议,我想知道这些建议是否应该被认真对待。

4

4 回答 4

5

GCC 允许并接受它。它通常不会被忽略(你知道这一点,因为 GCC 总是输出warning: attribute ignored,当它完全忽略一个属性时,它不会在这里)。但也阅读了最后一段。

是否有意义是另一个问题。虚函数可以被重载,你可以在没有属性的情况下重载它。这就引发了下一个问题:合法吗?

人们会期望具有不同属性的函数具有不同的签名(例如使用const限定符或不同的异常规范),但事实并非如此。GCC 在这方面将它们视为完全相同。您可以通过派生和实现成员函数 non-constBar来验证这一点。Foo然后

decltype(&Bar::square) f1 = &Foo::square;
decltype(&Foo::square) f2 = &Bar::square;

正如您所期望的那样,将在第二行给出编译时错误,但不会在第一行给出。如果签名不同(尝试使函数具有 const 限定,而不是使用属性!),第一行已经给出了错误。

最后,它是否安全,是否有意义?它总是安全的,编译器必须确保它是安全的。它在语义上确实有意义,在一定范围内。

从语义的角度来看,声明一个函数const或者pure如果它确实是这样是“正确的”。但是,就您向界面的用户做出可能不真实的“承诺”而言,这有点尴尬。有人可能会调用这个函数,这个函数看起来是const在派生类上,但事实并非如此。编译器必须确保它仍然可以工作,但用户对性能的期望可能与现实有所不同。

将函数标记为constpure可能允许编译器更好地优化。现在,使用虚函数,这有点困难,因为对象可能是派生类型,而这不是真的!
这必然意味着编译器必须忽略优化的属性,除非虚拟调用可以静态解析。这种情况可能仍然经常发生,但并非普遍如此。

于 2012-08-16T12:23:43.163 回答
5

第一个问题是这些属性是否甚至对虚拟方法具有有效的语义。在我看来,他们确实如此。我希望如果一个虚函数被标记为纯,你会向编译器保证所有实现只依赖于它们的参数和全局内存中的数据(并且不要改变它),其中全局内存中的数据也将包括物体。如果一个虚函数被标记为 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 中删除继承(从而使方法成为非虚拟方法)允许编译器进行预期的优化。

没有关于这些属性的标准或文档,因此我们可以拥有的最佳参考是实现。由于它们目前没有效果,我建议避免在虚拟方法上使用它们,以防将来行为发生意外变化。

于 2012-08-16T12:46:03.293 回答
3

在您链接到的文档中,在 const 属性的描述下有此注释:

请注意,具有指针参数并检查指向的数据的函数不得声明为 const。

我会说这包括成员函数,因为它们有一个隐式指针参数(至少虚函数需要检查它才能到达 vtable,不是吗?)。

他们似乎在这个线程中得出了类似的结论:http: //gcc.gnu.org/ml/gcc/2011-02/msg00460.html

于 2012-06-12T12:31:32.820 回答
2

当且仅当函数通过静态绑定调用时,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)
}
于 2013-08-23T04:28:14.437 回答