c++ - operaciones - punteros void lenguaje c
¿Por qué se permiten los punteros a las funciones en línea? (5)
1) ¿Por qué se permiten punteros a funciones en línea en c ++?
Debido a que las funciones en línea son funciones como cualquier otra, y señalarlas es una de las cosas que puede hacer con las funciones. Las funciones en línea simplemente no son especiales a este respecto.
He leído que el código de las funciones en línea acaba de copiarse en la instrucción de llamada a la función y no hay asignaciones de memoria de tiempo de compilación en las funciones en línea.
Usted (y quizás el material que ha leído) ha mezclado dos conceptos relacionados y con nombres similares.
Una función en línea se define en todas las unidades de traducción que la usan, mientras que una función no en línea se define en una unidad de traducción solo como lo requiere la regla de una definición. Eso es lo que significa una declaración en línea de una función; relaja la regla de una definición, pero también da el requisito adicional de ser definida en todas las unidades de traducción que la usan (lo que no hubiera sido posible si la ODR no estuviera relajada).
La expansión en línea (o en línea) es una optimización, donde se evita una llamada a la función copiando la función llamada en el marco de la persona que llama. Una llamada de función puede expandirse en línea, ya sea que la función haya sido declarada en línea o no. Y una función que se ha declarado en línea no se expande necesariamente en línea.
Sin embargo, una función no puede expandirse en línea en una unidad de traducción donde no está definida (a menos que la optimización del tiempo de enlace realice la expansión). Por lo tanto, el requisito de ser definido en todas las TU que permite la declaración en línea, también hace posible la expansión en línea de la función al permitir que la función se defina en todas las TU que la invocan. Pero la optimización no está garantizada.
2) ¿No debería imprimir diferentes valores de dirección de n cada vez que se llama a func ()?
La expansión en línea hace que las variables locales se ubiquen en el marco de la persona que llama, sí. Pero su ubicación diferirá independientemente de la expansión si las llamadas se originan en tramas separadas.
Por lo general, se genera una versión regular no expandida de cualquier función que se ha expandido en línea. Si se toma la dirección de una función, apuntará a esa función no expandida. Si el compilador puede probar que todas las llamadas a una función están en línea, el compilador podría optar por no proporcionar la versión no expandida. Esto requiere que la función tenga un enlace interno, y tomar la dirección de la función típicamente hace que tal prueba sea muy difícil o imposible.
Tengo dos preguntas:
1) ¿Por qué se permiten punteros a funciones en línea en C ++? He leído que el código de las funciones en línea simplemente se copia en la instrucción de llamada de función y no hay asignación de memoria en tiempo de compilación en las funciones en línea. Entonces, ¿por qué puede existir un puntero a una función en línea, dado que no hay una dirección de memoria fija para las funciones en línea?
2) Considere el siguiente código:
inline void func()
{
int n=0;
cout<<(&n);
}
¿No debería imprimir valores diferentes de la dirección de
n
cada vez que se llama a
func()
?
[Porque creo que cada vez que se copia el código de función en línea, se debe reasignar las variables locales (mientras que en el caso de las funciones normales, se lleva a cabo la reinicialización)]
Soy un principiante y formulé esta pregunta por el fortalecimiento de mi concepto. Por favor, corrígeme si me equivoco en alguna parte.
Además del punto ya mencionado de que una función en
inline
no necesita estar realmente en línea (y muchas funciones sin en
inline
están en
línea por los compiladores modernos), también es totalmente concebible en línea
una llamada a través de un puntero de función
.
Ejemplo:
#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;
}
Aquí,
foo(succ, i)
en su conjunto
podría alinearse a solo
i+1
.
Y de hecho, eso parece suceder
†
:
g++ -O3 -S
produce código para las funciones
foo
y
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
Pero luego genera código para
main
que
nunca se refiere a
ninguno de estos, sino que incluye un nuevo
_GLOBAL__sub_I__Z3fooPFiiEi
especializado:
.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
Entonces, en este caso, el programa real ni siquiera contiene un puntero de función que apunte a
succ
: el compilador descubrió que este puntero siempre se referiría a la misma función de todos modos y, por lo tanto, pudo eliminar todo sin cambiar el comportamiento.
Esto puede mejorar mucho el rendimiento, cuando a menudo se llaman funciones pequeñas a través de punteros de función.
Lo cual es una técnica bastante extendida en lenguajes funcionales;
Los compiladores para idiomas como O''Caml y Haskell hacen un gran uso de este tipo de optimización.
† Descargo de responsabilidad: mis habilidades de ensamblaje son casi inexistentes. Bien podría estar hablando basura aquí.
La
palabra clave en
inline
fue originalmente una pista para el compilador de que usted, el programador, cree que esta función es candidata para la inclusión en línea: no es necesario que el compilador cumpla con esto.
En el uso moderno, tiene poco o nada que ver con la integración: los compiladores modernos incorporan libremente (o no) funciones "detrás de usted", que forman parte de las técnicas de optimización.
Las transformaciones de código (incluida la alineación) se realizan bajo la regla "como si" en C ++ , lo que básicamente significa que el compilador puede transformar el código como lo desee, siempre y cuando la ejecución sea "como si" se ejecutó el código original como esta escrito. Esta regla alimenta las optimizaciones en C ++.
Dicho esto, una vez que se toma una dirección de una función, se requiere que exista (es decir, se requiere que la dirección sea válida). Esto puede significar que ya no está en línea, pero aún podría estarlo (el optimizador aplicará el análisis apropiado).
Entonces, ¿por qué puede existir un puntero a una función en línea, dado que no hay una dirección de memoria fija de las funciones en línea?
No, es solo una pista y se relaciona en gran medida con los vínculos y no con la alineación real. Esto alimenta, lo que podría decirse que es el uso actual principal, definiendo funciones en archivos de encabezado.
¿No debería imprimir diferentes valores de dirección de
n
cada vez que se llama afunc()
?
Es posible que
n
sea una variable local, basada en la ubicación de la pila cuando se ejecuta la función.
Dicho esto, la función en
inline
, se relaciona con el enlace, el enlazador fusionará las funciones sobre las unidades de traducción.
Como se señaló en los comentarios ;
... que si el ejemplo se cambia a
static int n
, entonces cada llamada a la función debe imprimir un valor constante (en un solo programa, por supuesto) ... y eso es cierto independientemente de si el código está o no en línea .
Este es, nuevamente, el efecto del requisito de vinculación en la variable local
n
.
Las funciones en línea no siempre están en línea. Solo indica que el programador desea que esta función esté en línea. El compilador puede alinear cualquier función, independientemente de si se utilizó o no una palabra clave en línea.
Si se utiliza la dirección de la función, lo más probable es que la función no esté incorporada en el ejecutable final, al menos en GCC:
Cuando una función es tanto en línea como estática, si todas las llamadas a la función están integradas en la persona que llama, y la dirección de la función nunca se usa, entonces el código del ensamblador de la función nunca se hace referencia.
Lees material antiguo.
La razón principal para usar hoy
inline
día en
inline
es permitir cuerpos de funciones en archivos de encabezado.
El uso de una palabra clave en
inline
con una función le indica al enlazador que todas las instancias de la función en las unidades de traducción pueden combinarse;
tener una función no en línea en un encabezado que se incluye desde varias unidades provoca un comportamiento indefinido debido a una violación de la regla de una definición.
C ++ 17 también agrega variables en línea , que tienen la misma propiedad que la variable se puede definir en un encabezado, y el enlazador combina todas las definiciones en lugar de causar una violación de ODR.
Las cosas de las que está hablando con "el código que se copia en la función de llamada" se llama en línea y son independientes de la palabra clave en
inline
.
El compilador decidirá si hacer esto o no, en función de la configuración de optimización, para funciones no en línea, así como funciones en línea.