¿Es "en línea" sin "estático" o "externo" alguna vez útil en C99?

96

Cuando intento compilar este código

inline void f() {}

int main()
{
    f();
}

usando la línea de comando

gcc -std=c99 -o a a.c

Recibo un error del vinculador (referencia indefinida a f). El error desaparece si uso static inlineo en extern inlinelugar de solo inline, o si compilo con -O(por lo que la función está en línea).

Este comportamiento parece estar definido en el párrafo 6.7.4 (6) de la norma C99:

Si todas las declaraciones de alcance de archivo para una función en una unidad de traducción incluyen el inlineespecificador de función sin extern, entonces la definición en esa unidad de traducción es una definición en línea. Una definición en línea no proporciona una definición externa para la función y no prohíbe una definición externa en otra unidad de traducción. Una definición en línea proporciona una alternativa a una definición externa, que un traductor puede utilizar para implementar cualquier llamada a la función en la misma unidad de traducción. No se especifica si una llamada a la función utiliza la definición en línea o la definición externa.

Si entiendo todo esto correctamente, una unidad de compilación con una función definida inlinecomo en el ejemplo anterior solo se compila de manera consistente si también hay una función externa con el mismo nombre, y nunca sé si se llama a mi propia función o la función externa.

¿No es este comportamiento completamente tonto? ¿Es útil definir una función inlinesin statico externen C99? ¿Me estoy perdiendo de algo?

Resumen de respuestas

Por supuesto que me estaba perdiendo algo, y el comportamiento no es tonto. :)

Como explica Nemo , la idea es poner la definición de la función

inline void f() {}

en el archivo de encabezado y solo una declaración

extern inline void f();

en el archivo .c correspondiente. Solo la externdeclaración activa la generación de código binario visible externamente. Y, de hecho, no se usa inlineen un archivo .c, solo es útil en los encabezados.

Como explica el razonamiento del comité C99 citado en la respuesta de Jonathan , inlinese trata de optimizaciones del compilador que requieren que la definición de una función sea visible en el sitio de una llamada. Esto solo se puede lograr colocando la definición en el encabezado y, por supuesto, una definición en un encabezado no debe emitir código cada vez que el compilador la ve. Pero dado que el compilador no está obligado a incorporar una función, debe existir una definición externa en alguna parte.

Sven Marnach
fuente
duplicado límite de stackoverflow.com/questions/5369888/…
Earlz
@Earlz: Gracias por el enlace. Mi pregunta es sobre la razón fundamental detrás del párrafo citado de la norma, y ​​si alguna vez hay casos de uso inlinesin staticy extern, sin embargo. Desafortunadamente, ninguno de estos temas se aborda en esa pregunta.
Sven Marnach
@Nemo: Leí esa pregunta y las respuestas antes de publicar la mía. Una vez más, no estoy preguntando "¿cómo se comporta?" sino más bien "¿cuál es la idea detrás de este comportamiento"? Estoy bastante seguro de que me falta algo aquí.
Sven Marnach
1
sí, por eso dije "límite". Personalmente, creo que esto es una pregunta completamente diferente
Earlz

Respuestas:

40

En realidad, esta excelente respuesta también responde a su pregunta, creo:

¿Qué hace extern inline?

La idea es que "en línea" se puede usar en un archivo de encabezado y luego "en línea externa" en un archivo .c. "extern inline" es simplemente cómo le indica al compilador qué archivo de objeto debe contener el código generado (visible externamente).

[actualización, para elaborar]

No creo que haya ningún uso para "en línea" (sin "estático" o "externo") en un archivo .c. Pero en un archivo de encabezado tiene sentido, y requiere una declaración "externa en línea" correspondiente en algún archivo .c para generar realmente el código independiente.

Nemo
fuente
1
¡Gracias, empiezo a tener la idea!
Sven Marnach
8
Sí, nunca entendí esto hasta ahora, así que gracias por preguntar :-). Es extraño que la definición "en línea" no externa vaya en el encabezado (pero no necesariamente resulte en ninguna generación de código), mientras que la declaración "en línea externa" va en el archivo .c y en realidad hace que el código ser generado.
Nemo
3
¿Significa esto que escribo inline void f() {}en el encabezado y extern inline void f();en el archivo .c? Entonces, ¿la definición de función real va en el encabezado y el archivo .c contiene una mera declaración en este caso, en orden inverso al habitual?
Sven Marnach
1
@endolith: No entiendo tu pregunta. Estamos discutiendo inline sin static o extern. Por supuesto que static inlineestá bien, pero de eso no se trata esta pregunta y respuesta.
Nemo
2
@MatthieuMoy Necesitas usar en -std=c99lugar de -std=gnu89.
a3f
27

Del mismo estándar (ISO / IEC 9899: 1999):

Apéndice J.2 Comportamiento indefinido

  • ...
  • Una función con enlace externo se declara con un inlineespecificador de función, pero tampoco se define en la misma unidad de traducción (6.7.4).
  • ...

El Comité C99 escribió una Justificación y dice:

6.7.4 Especificadores de funciones

Una nueva característica de C99: la inlinepalabra clave, adaptada de C ++, es un especificador de función que solo se puede usar en declaraciones de función. Es útil para optimizaciones de programas que requieren que la definición de una función sea visible en el sitio de una llamada. (Tenga en cuenta que el Estándar no intenta especificar la naturaleza de estas optimizaciones).

La visibilidad está asegurada si la función tiene un enlace interno, o si tiene un enlace externo y la llamada está en la misma unidad de traducción que la definición externa. En estos casos, la presencia de la inlinepalabra clave en una declaración o definición de la función no tiene ningún efecto más allá de indicar una preferencia de que las llamadas de esa función deben optimizarse con preferencia a las llamadas de otras funciones declaradas sin la inlinepalabra clave.

La visibilidad es un problema para una llamada de una función con enlace externo donde la llamada está en una unidad de traducción diferente de la definición de la función. En este caso, la inlinepalabra clave permite que la unidad de traducción que contiene la llamada también contenga una definición local o en línea de la función.

Un programa puede contener una unidad de traducción con una definición externa, una unidad de traducción con una definición en línea y una unidad de traducción con una declaración pero sin definición para una función. Las llamadas en la última unidad de traducción utilizarán la definición externa como de costumbre.

Una definición en línea de una función se considera una definición diferente a la definición externa. Si se funcproduce una llamada a alguna función con enlace externo donde una definición en línea es visible, el comportamiento es el mismo que si la llamada se hiciera a otra función, por ejemplo __func, con enlace interno. Un programa conforme no debe depender de la función que se llame. Este es el modelo en línea en el estándar.

Un programa conforme no debe depender de la implementación usando la definición en línea, ni puede depender de la implementación usando la definición externa. La dirección de una función es siempre la dirección correspondiente a la definición externa, pero cuando esta dirección se usa para llamar a la función, se puede usar la definición en línea. Por lo tanto, es posible que el siguiente ejemplo no se comporte como se esperaba.

inline const char *saddr(void)
{
    static const char name[] = "saddr";
    return name;
}
int compare_name(void)
{
    return saddr() == saddr(); // unspecified behavior
}

Dado que la implementación puede usar la definición en línea para una de las llamadas saddry usar la definición externa para la otra, no se garantiza que la operación de igualdad se evalúe a 1 (verdadero). Esto muestra que los objetos estáticos definidos dentro de la definición en línea son distintos de su objeto correspondiente en la definición externa. Esto motivó la restricción en contra incluso de definir un no constobjeto de este tipo.

Inlining se agregó al estándar de tal manera que se puede implementar con la tecnología de enlazador existente, y un subconjunto de C99 inlining es compatible con C ++. Esto se logró al requerir que se especificara exactamente una unidad de traducción que contiene la definición de una función en línea como la que proporciona la definición externa para la función. Debido a que esa especificación consiste simplemente en una declaración que carece de la inlinepalabra clave o contiene ambas inliney extern, también será aceptada por un traductor de C ++.

Inlining en C99 extiende la especificación de C ++ de dos maneras. Primero, si una función se declara inlineen una unidad de traducción, no es necesario declararla inlineen todas las demás unidades de traducción. Esto permite, por ejemplo, una función de biblioteca que debe incluirse dentro de la biblioteca, pero está disponible solo a través de una definición externa en otro lugar. La alternativa de usar una función contenedora para la función externa requiere un nombre adicional; y también puede afectar negativamente al rendimiento si un traductor no realiza la sustitución en línea.

En segundo lugar, el requisito de que todas las definiciones de una función en línea sean "exactamente iguales" se reemplaza por el requisito de que el comportamiento del programa no debe depender de si una llamada se implementa con una definición en línea visible, o la definición externa, de un función. Esto permite que una definición en línea se especialice para su uso dentro de una unidad de traducción particular. Por ejemplo, la definición externa de una función de biblioteca puede incluir alguna validación de argumentos que no es necesaria para las llamadas realizadas desde otras funciones en la misma biblioteca. Estas extensiones ofrecen algunas ventajas; y los programadores que estén preocupados por la compatibilidad pueden simplemente cumplir con las reglas más estrictas de C ++.

Nótese que es no apropiado para implementaciones para proporcionar las definiciones de las funciones en línea de la biblioteca estándar en las cabeceras estándar ya que esto puede romper algo de código legado que redeclares funciones de la librería estándar después incluyendo sus cabeceras. La inlinepalabra clave está destinada únicamente a proporcionar a los usuarios una forma portátil de sugerir la inserción de funciones. Debido a que los encabezados estándar no necesitan ser portátiles, las implementaciones tienen otras opciones como:

#define abs(x) __builtin_abs(x)

u otros mecanismos no portátiles para incorporar funciones de biblioteca estándar.

Jonathan Leffler
fuente
Gracias, esto es muy detallado y casi tan legible como el estándar mismo. :-) Sabía que debía faltar algo. ¿Podría darnos una referencia de dónde sacó esto?
Sven Marnach
Simplemente se me pasó por la cabeza que puede usar Google para encontrar enlaces: open-std.org/jtc1/sc22/wg14/www/C99RationaleV5.10.pdf
Sven Marnach
@Sven: Tomé prestada la URL de tu búsqueda y la puse en la respuesta. El documento que utilicé fue una copia del razonamiento que guardé anteriormente (en 2005), pero también fue V5.10.
Jonathan Leffler
4
Gracias de nuevo. La información más valiosa que obtuve de la respuesta es que hay un documento que contiene la justificación del estándar C99 (y probablemente también hay documentos para otros estándares). Si bien la respuesta de Nemo me dio un pez, este me enseñó a pescar.
Sven Marnach
0

> Recibo un error del vinculador (referencia indefinida a f)

Funciona aquí: Linux x86-64, GCC 4.1.2. Puede ser un error en su compilador; No veo nada en el párrafo citado del estándar que prohíba el programa dado. Tenga en cuenta el uso de if en lugar de iff .

Una definición en línea proporciona una alternativa a una definición externa , que un traductor puede utilizar para implementar cualquier llamada a la función en la misma unidad de traducción.

Por lo tanto, si conoce el comportamiento de la función fy desea llamarla en un bucle cerrado, puede copiar y pegar su definición en un módulo para evitar llamadas a funciones; o , puede proporcionar una definición que, para los propósitos del módulo actual, sea equivalente (pero omite la validación de entrada o cualquier optimización que pueda imaginar). El escritor del compilador, sin embargo, tiene la opción de optimizar el tamaño del programa.

Fred Foo
fuente
2
El estándar establece que el compilador siempre puede asumir que hay una función externa con el mismo nombre y llamarla en lugar de la versión en línea, así que no creo que sea un error del compilador. Probé con gcc 4.3, 4.4 y 4.5, todos dan un error de enlazador.
Sven Marnach