Почему указатели на встроенные функции разрешены?
У меня есть два вопроса:
1) Почему указатели на встроенные функции разрешены на С++?
Я прочитал, что код встроенных функций просто копируется в оператор вызова функции, и в встроенных функциях отсутствует распределение памяти во время компиляции. Итак, почему указатель существует для встроенной функции, учитывая, что для встроенных функций нет фиксированного адреса памяти?
2) Рассмотрим приведенный ниже код:
inline void func()
{
int n=0;
cout<<(&n);
}
Должен ли он не печатать разные значения адреса n
каждый раз, когда вызывается func()
?
[Поскольку я думаю, что каждый раз, когда код встроенной функции копируется, необходимо выполнить перераспределение локальных переменных (тогда как в случае нормальных функций происходит повторная инициализация)]
Я новичок, и я задал этот вопрос ради укрепления моей концепции. Пожалуйста, поправьте меня, если я где-то не прав.
Ответы
Ответ 1
1) Почему указатели на встроенные функции разрешены в С++?
Поскольку встроенные функции являются функциями, как и любые другие, и указывая на них, это одна из вещей, которые вы можете делать с функциями. Встроенные функции просто не являются особенными в этом отношении.
Я прочитал, что код встроенных функций только что скопирован в оператор вызова функции, и нет встроенных функций памяти во встроенных функциях.
Вы (и, возможно, материал, который вы прочитали) смешали две связанные и аналогично названные концепции.
Встроенная функция определена во всех единицах трансляции, которые ее используют, в то время как не-встроенная функция определена в одной единицы перевода только в соответствии с требованиями одного правила определения. Это то, что означает встроенное объявление функции; он ослабляет одно правило определения, но также дает дополнительное требование быть определенным во всех единицах перевода, которые его используют (что было бы невозможно, если odr не расслабился).
Встроенное расширение (или inlining) - это оптимизация, при которой вызов функции исключается путем копирования вызываемой функции в кадр вызывающего. Вызов функции может быть расширен внутри, независимо от того, была ли функция объявлена встроенной или нет. И функция, которая была объявлена встроенной, не обязательно расширяется в строке.
Тем не менее, не встроенная функция не может быть развернута внутри линии перевода, где она не определена (если оптимизация времени соединения не выполняет расширение). Поэтому требование определения во всех TU, разрешенных встроенной декларацией, также делает возможным встроенное расширение функции через единицы перевода. Но оптимизация не гарантируется.
2) Если он не печатает разные значения адреса n каждый раз, когда вызывается функция func()?
Внутреннее расширение действительно приводит к тому, что локальные переменные будут расположены в кадре вызывающего абонента, да. Но их местоположение будет отличаться независимо от расширения, если вызовы происходят из отдельных кадров.
Обычно существует обычная нерасширенная версия, сгенерированная любой функцией, которая была расширена в строке. Если адрес функции будет выполнен, он будет указывать на нерасширенную функцию. Если компилятор может доказать, что все вызовы функции встроены, компилятор может вообще не предоставлять нерасширенную версию. Это требует, чтобы функция имела внутреннюю привязку, а определение адреса функции обычно делает такие доказательства очень трудными или невозможными.
Ответ 2
inline
keyword изначально было намеком на компилятор, который вы, программист, считаете, что эта функция является кандидатом на inlining - компилятор не требуется соблюдать это.
В современном использовании он практически не имеет никакого отношения к инкрустации - современные компиляторы свободно встроенные (или не) функции "за вами", они составляют часть методов оптимизации.
Преобразования кода (включая inlining) выполняются в "as-if" правило в С++, что в основном означает, что компилятор может преобразовать код так, как он хочет, пока выполнение "как есть", исходный код был выполнен как написанный. Это правило поддерживает оптимизацию в С++.
Тем не менее, как только адрес берется с функцией, он должен существовать (т.е. адрес должен быть действительным). Это может означать, что он больше не встроен, но все еще может быть (оптимизатор применит соответствующий анализ).
Итак, почему может существовать указатель на встроенную функцию, если нет фиксированного адреса памяти встроенных функций?
Нет, это всего лишь намек и во многом связан с привязкой, а не с фактической инкрустацией. Это топливо, что, возможно, является основным текущим использованием, определяющим функции в файлах заголовков.
Если он не печатает разные значения адреса n
каждый раз, когда вызывается func()
?
Он может, n
- это локальная переменная, основанная на местоположении стека при выполнении функции. Тем не менее, функция inline
, она связана с привязкой, компоновщик объединит функции над единицами трансляции.
Как отмечено в комментариях;
... что если этот пример изменен на static int n
, то каждый вызов функции должен печатать постоянное значение (в одном прогоне программы, конечно)... и это правда, является ли код inlined или нет.
Это опять-таки эффект требования привязки к локальной переменной n
.
Ответ 3
Вы читаете старый материал. Основная причина использования inline
nowdays - разрешить телам функций в файлах заголовков. Использование ключевого слова inline
с функцией сигнализирует компоновщику, что все экземпляры функции между единицами перевода могут быть объединены; наличие не-встроенной функции в заголовке, включенном из нескольких блоков, вызывает поведение undefined из-за нарушения правила определения.
С++ 17 также добавляет встроенные переменные, которые имеют то же свойство, что переменная может быть определена в заголовке, и все определения объединяются компоновщиком вместо нарушения ODR.
То, о чем вы говорите, "код, скопированный в вызывающую функцию", называется вложением и не зависит от ключевого слова inline
. Компилятор решит, следует ли делать это, основываясь на настройках оптимизации, для не-встроенных функций, а также встроенных функций.
Ответ 4
Встроенные функции не всегда встроены. Это просто означает, что программист хотел бы, чтобы эта функция была встроена. Компилятор имеет возможность встраивать любую функцию, независимо от того, использовалось ли ключевое слово inline или нет.
Если используется адрес функции, функция, скорее всего, не включена в окончательный исполняемый файл, по крайней мере, в GCC:
Когда функция является как встроенной, так и статической, если все вызовы функции интегрированы в вызывающий, а адрес функции никогда не используется, тогда собственный код ассемблера функции никогда не ссылается.
Документация GCC
Ответ 5
Помимо уже упомянутой точки, что функция inline
не обязательно должна быть встроена (и многие функции без inline
встроены современными компиляторами), она также вполне возможна для встроенного вызова через указатель функции. Пример:
#include <iostream>
int foo(int (*fun)(int), int x) {
return fun(x);
}
int succ(int n) {
return n+1;
}
int main() {
int c=0;
for (int i=0; i<10000; ++i) {
c += foo(succ, i);
}
std::cout << c << std::endl;
}
Здесь foo(succ, i)
может быть в целом привязано только к i+1
. И действительно, это похоже на †: g++ -O3 -S
создает код для функций foo
и succ
_Z3fooPFiiEi:
.LFB998:
.cfi_startproc
movq %rdi, %rax
movl %esi, %edi
jmp *%rax
.cfi_endproc
.LFE998:
.size _Z3fooPFiiEi, .-_Z3fooPFiiEi
.p2align 4,,15
.globl _Z4succi
.type _Z4succi, @function
_Z4succi:
.LFB999:
.cfi_startproc
leal 1(%rdi), %eax
ret
.cfi_endproc
Но затем он генерирует код для main
, который никогда не ссылается ни на одно из них, а просто включает в себя новый специализированный _GLOBAL__sub_I__Z3fooPFiiEi
:
.LFE999:
.size _Z4succi, .-_Z4succi
.section .text.startup,"ax",@progbits
.p2align 4,,15
.globl main
.type main, @function
main:
.LFB1000:
.cfi_startproc
movdqa .LC1(%rip), %xmm4
xorl %eax, %eax
pxor %xmm1, %xmm1
movdqa .LC0(%rip), %xmm0
movdqa .LC2(%rip), %xmm3
jmp .L5
.p2align 4,,10
.p2align 3
.L8:
movdqa %xmm2, %xmm0
.L5:
movdqa %xmm0, %xmm2
addl $1, %eax
paddd %xmm3, %xmm0
cmpl $2500, %eax
paddd %xmm0, %xmm1
paddd %xmm4, %xmm2
jne .L8
movdqa %xmm1, %xmm5
subq $24, %rsp
.cfi_def_cfa_offset 32
movl $_ZSt4cout, %edi
psrldq $8, %xmm5
paddd %xmm5, %xmm1
movdqa %xmm1, %xmm6
psrldq $4, %xmm6
paddd %xmm6, %xmm1
movdqa %xmm1, %xmm7
movd %xmm7, 12(%rsp)
movl 12(%rsp), %esi
call _ZNSolsEi
movq %rax, %rdi
call _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
xorl %eax, %eax
addq $24, %rsp
.cfi_def_cfa_offset 8
ret
.cfi_endproc
.LFE1000:
.size main, .-main
.p2align 4,,15
.type _GLOBAL__sub_I__Z3fooPFiiEi, @function
_GLOBAL__sub_I__Z3fooPFiiEi:
.LFB1007:
.cfi_startproc
subq $8, %rsp
.cfi_def_cfa_offset 16
movl $_ZStL8__ioinit, %edi
call _ZNSt8ios_base4InitC1Ev
movl $__dso_handle, %edx
movl $_ZStL8__ioinit, %esi
movl $_ZNSt8ios_base4InitD1Ev, %edi
addq $8, %rsp
.cfi_def_cfa_offset 8
jmp __cxa_atexit
.cfi_endproc
.LFE1007:
.size _GLOBAL__sub_I__Z3fooPFiiEi, .-_GLOBAL__sub_I__Z3fooPFiiEi
.section .init_array,"aw"
.align 8
.quad _GLOBAL__sub_I__Z3fooPFiiEi
.local _ZStL8__ioinit
.comm _ZStL8__ioinit,1,1
Таким образом, в этом случае фактическая программа даже не содержит указателя функции, указывающего на succ
- компилятор обнаружил, что этот указатель всегда будет ссылаться на одну и ту же функцию в любом случае и поэтому смог устранить всю вещь без изменение поведения. Это может значительно повысить производительность, когда вы часто вызываете небольшие функции с помощью указателей функций. Это довольно распространенный метод на функциональных языках; компиляторы для таких языков, как O'Caml и Haskell, используют эту оптимизацию.
† Отказ от ответственности: мои навыки сборки близки к несуществующим. Я вполне мог бы говорить здесь мусор.