Функция 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)
}