Функция GCC __attribute__s работает с виртуальными функциями?

Компилятор GCC С++ предлагает семейство расширений через атрибуты функций, например:

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 создавать предложения о том, где эти атрибуты могут быть помещены для улучшения кода. Тем не менее, похоже, в конечном итоге эти предложения предлагаются даже для виртуальных функций, и мне интересно, следует ли серьезно относиться к этим предложениям.

Ответ 1

Первый вопрос заключается в том, имеют ли эти атрибуты действительную семантику для виртуальных методов. По-моему, они это делают. Я ожидал бы, что если бы виртуальная функция была помечена как чистая, вы бы обещают компилятору, что все реализации полагаются только на свои аргументы и данные в глобальной памяти (и не меняют это), где данные в глобальной памяти также будут включать содержимое объект. Если бы виртуальная функция была помечена как 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 (и, таким образом, делает метод не виртуальным) позволяет компилятору выполнить ожидаемую оптимизацию.

Нет никаких стандартов или документации относительно этих атрибутов, поэтому наилучшей ссылкой, которую мы можем получить, является реализация. Поскольку они в настоящее время не имеют эффекта, я бы предложил избегать их использования на виртуальных методах в случае непредвиденного изменения поведения в будущем.

Ответ 2

Разрешено и принято GCC. Обычно это игнорируется (вы знаете это, потому что GCC всегда выводит warning: attribute ignored, когда он полностью игнорирует атрибут, его здесь нет). Но также прочитайте последний абзац.

Имеет ли смысл другой вопрос. Виртуальная функция может быть перегружена, и вы можете перегрузить ее без атрибута. Это открывает следующий вопрос: является ли это законным?

Можно ожидать, что функция с разными атрибутами будет иметь другую подпись (например, с квалификатором const или другой спецификацией исключения), но это не так. GCC рассматривает их как абсолютно идентичные в этом отношении. Вы можете проверить это, выведя Bar из Foo и реализуя функцию-член non-const. Тогда

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

Дает ошибку во время компиляции во второй строке, но не в первой, как и следовало ожидать. Если разные подписи (попробуйте сделать функцию const-qual, вместо использования атрибута!), Первая строка уже выдала бы ошибку.

Наконец, это безопасно, и имеет ли это смысл? Это всегда безопасно, компилятор должен убедиться в этом. Это имеет смысл семантически, в пределах.

С семантической точки зрения "правильно" объявить функцию const или pure, если это так. Однако это неловко, поскольку вы делаете "обещание" пользователю интерфейса, который может быть неверным. Кто-то может назвать эту функцию, которая, по всей видимости, const в производном классе, где это неверно. Компилятор должен будет убедиться, что он все еще работает, но ожидания пользователей от производительности могут отличаться от реальности.

Функции маркировки как const или pure, возможно, позволяют оптимизировать компилятор. Теперь с помощью виртуальной функции это несколько сложно, поскольку объект может иметь производный тип, если это неверно!
Это обязательно означает, что компилятор должен игнорировать атрибут для оптимизации, если виртуальный вызов не может быть решен статически. Это может все еще иметь место, но не в целом.

Ответ 3

В документе, к которому вы привязались, есть эта заметка в описании атрибута const:

Обратите внимание, что функция, имеющая аргументы указателя и проверяющая указанные данные, не должна быть объявлена ​​const.

Я бы сказал, что это включает в себя функции-члены, поскольку они имеют неявный параметр указателя (и, по крайней мере, виртуальные функции должны проверять его, чтобы добраться до vtable, нет?).

Они, похоже, доходят до аналогичного вывода в этой теме: http://gcc.gnu.org/ml/gcc/2011-02/msg00460.html

Ответ 4

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)
}