Es & errno legal C?
language-lawyer (5)
Así que §6.5.3.2p1 especifica
El operando del operador unario será un designador de función, el resultado de un operador [] o unario *, o un valor l que designa un objeto que no es un campo de bits y no se declara con el especificador de clase de almacenamiento de registro .
Lo cual creo que puede interpretarse en el sentido de que &lvalue
está bien para cualquier lvalue que no esté en esas dos categorías. Y como mencionaste, errno
no se puede declarar con el especificador de clase de almacenamiento de registro, y creo (aunque no estoy buscando referencias para verificar en este momento) que no puedes tener un campo de bits que tenga tipo de int
plano.
Así que creo que la especificación requiere &(errno)
ser legal C.
Si & errno es legal C, ¿se requiere que sea constante?
Según entiendo, parte del hecho de permitir que errno
sea una macro (y la razón por la que está en eg glibc) es permitir que sea una referencia al almacenamiento local de subprocesos, en cuyo caso no será constante a través de trapos. Y no veo ninguna razón para esperar que deba ser constante. Siempre que el valor de errno
conserve la semántica especificada, no veo ninguna razón para que una biblioteca C perversa no pueda cambiar &errno
para referirse a diferentes direcciones de memoria en el curso de un programa, por ejemplo, liberando y reasignando la tienda de respaldo cada vez que configura errno
.
Se podría imaginar el mantenimiento de un buffer en anillo de los últimos valores de N errno establecidos por la biblioteca, y tener &errno
siempre apuntando a lo último. No creo que sea particularmente útil, pero no veo ninguna forma de violar las especificaciones.
Por 7.5,
[errno] se expande a un lvalue175 modificable) que tiene tipo int, cuyo valor se establece en un número de error positivo por varias funciones de la biblioteca. No se especifica si errno es una macro o un identificador declarado con un enlace externo. Si se suprime una definición de macro para acceder a un objeto real, o un programa define un identificador con el nombre errno, el comportamiento no está definido.
175) El macro errno no necesita ser el identificador de un objeto. Podría expandirse a un valor l modificable resultante de una llamada a función (por ejemplo, * errno ()).
No está claro para mí si esto es suficiente para exigir que &errno
sea una violación de restricción. El lenguaje C tiene valores l (como variables de clase de almacenamiento de registro, sin embargo, estos solo pueden ser automáticos, por lo que errno
no se pudo definir como tal) para los cuales el operador &
es una violación de restricción.
Si &errno
es legal C, ¿se requiere que sea constante?
Esto parece una implementación válida donde &errno
sería una violación de restricción:
struct __errno_struct {
signed int __val:12;
} *__errno_location(void);
#define errno (__errno_location()->__val)
Entonces creo que la respuesta es probablemente no ...
La implementación original de errno
era como una variable global int que varios componentes de la biblioteca estándar C utilizaban para indicar un valor de error si se producía un error. Sin embargo, incluso en aquellos días, había que tener cuidado con el código de reentrada o con las llamadas a la función de la biblioteca que podrían establecer errno
en un valor diferente ya que manejaba un error. Normalmente, uno guardará el valor en una variable temporal si el código de error fue necesario por cualquier período de tiempo debido a la posibilidad de que alguna otra función o fragmento de código establezca el valor de errno
explícitamente o mediante una llamada a función de biblioteca.
Entonces, con esta implementación original de un int global, usar la dirección del operador y dependiendo de la dirección para permanecer constante estaba más o menos integrado en el tejido de la biblioteca.
Sin embargo, con multi-threading, ya no había un solo global porque tener un único global no era seguro para subprocesos. De modo que la idea de tener un almacenamiento local de subprocesos quizás utilizando una función que devuelve un puntero a un área asignada. Por lo tanto, es posible que vea una construcción como el siguiente ejemplo completamente imaginario:
#define errno (*myErrno())
typedef struct {
// various memory areas for thread local stuff
int myErrNo;
// more memory areas for thread local stuff
} ThreadLocalData;
ThreadLocalData *getMyThreadData () {
ThreadLocalData *pThreadData = 0; // placeholder for the real thing
// locate the thread local data for the current thread through some means
// then return a pointer to this thread''s local data for the C run time
return pThreadData;
}
int *myErrno () {
return &(getMyThreadData()->myErrNo);
}
Entonces, errno
se usaría como si fuera una sola variable int global en lugar de una cadena segura por errno = 0;
o revisándolo como if (errno == 22) { // handle the error
e incluso algo como int *pErrno = &errno;
. Todo esto funciona porque al final el área de datos local de subprocesos se asigna y se mantiene y no se mueve, y la definición de macro que hace que errno
parezca un extern int
oculta la fontanería de su implementación real.
Lo que no queremos es que la dirección de errno
cambie repentinamente entre divisiones temporales de un hilo con algún tipo de asignación dinámica, clonar, eliminar secuencia mientras estamos accediendo al valor. Cuando su intervalo de tiempo está activo, está activo y, a menos que tenga algún tipo de sincronización involucrada o alguna forma de mantener la CPU después de que caduque su intervalo de tiempo, hacer que el área local del subproceso se mueva parece una proposición muy arriesgada para mí.
Esto a su vez implica que puede depender de la dirección del operador que le da un valor constante para un hilo en particular, aunque el valor constante diferirá entre los hilos. Puedo ver la biblioteca usando la dirección de errno
para reducir la sobrecarga de hacer algún tipo de búsqueda local de subprocesos cada vez que se llama a una función de biblioteca.
Tener la dirección de errno
como constante dentro de un hilo también proporciona compatibilidad con versiones anteriores del código fuente anterior que utilizaba el archivo de inclusión errno.h como debería haberlo hecho (consulte esta página de manual de linux para errno que advierte explícitamente a no usar extern int errno;
como era común en los viejos tiempos).
La forma en que leo el estándar es para permitir este tipo de almacenamiento local de subprocesos mientras se mantiene la semántica y la sintaxis similar al antiguo extern int errno;
cuando se usa errno
y permite el uso anterior así como algún tipo de compilador cruzado para un dispositivo integrado que no admite multi-threading. Sin embargo, la sintaxis puede ser similar debido al uso de una definición de macro por lo que la declaración de atajo de estilo antiguo no debe utilizarse porque esa declaración no es lo que realmente es el errno
real.
Me sorprende que nadie haya citado la especificación C11 todavía. Disculpas por la cita larga, pero creo que es relevante.
7.5 Errores
El encabezado define varias macros ...
...y
errno
que se expande a un lvalue modificable (201) que tiene tipo
int
y duración de almacenamiento local de subprocesos, cuyo valor se establece en un número de error positivo por varias funciones de biblioteca. Si se suprime una definición de macro para acceder a un objeto real, o un programa define un identificador con el nombreerrno
, el comportamiento no está definido.El valor de
errno
en el hilo inicial es cero al inicio del programa (el valor inicial deerrno
en otros hilos es un valor indeterminado), pero nunca se establece en cero por ninguna función de biblioteca. (202) El valor de errno puede configurarse como no cero por una función de biblioteca llama si hay o no un error, siempre que el uso deerrno
no esté documentado en la descripción de la función en esta Norma Internacional.(201) El macro
errno
no necesita ser el identificador de un objeto. Podría expandirse a un valor l modificable resultante de una llamada a función (por ejemplo,*errno()
).(202) Por lo tanto, un programa que utiliza
errno
para la comprobación de errores debe establecerlo en cero antes de una llamada a función de biblioteca, luego inspeccionarlo antes de una llamada de función de biblioteca posterior. Por supuesto, una función de biblioteca puede guardar el valor deerrno
en la entrada y luego establecerlo en cero, siempre que se restaure el valor original si el valor deerrno
sigue siendo cero justo antes de la devolución.
"Hilo local" significa que el register
está fuera. Tipo int
significa que los campos de bits están fuera (IMO). Entonces, &errno
parece legal.
El uso persistente de palabras como "it" y " the value" sugiere que los autores del estándar no contemplaron &errno
no son constantes. Supongo que uno podría imaginar una implementación donde &errno
no era constante dentro de un hilo en particular, pero para ser usado como dicen las notas al pie (poner a cero, luego verificar después de llamar a la función de biblioteca), tendría que ser deliberadamente adversarial, y posiblemente requerir soporte de compilador especializado solo para ser adversarial.
En resumen, si la especificación permite un error no constante &errno
, no creo que haya sido deliberado.
[actualizar]
R. hace una excelente pregunta en los comentarios. Después de pensarlo, creo que ahora sé la respuesta correcta a su pregunta y a la pregunta original. Déjame ver si puedo convencerte, querido lector.
R. señala que GCC permite algo como esto en el nivel superior:
register int errno asm ("r37"); // line R
Esto declararía errno
como un valor global retenido en el registro r37
. Obviamente, sería un valor lvalue modificable localmente. Entonces, ¿podría una implementación C conforme declarar errno
así?
La respuesta es no . Cuando usted o yo usamos la palabra "declaración", generalmente tenemos un concepto coloquial e intuitivo en mente. Pero el estándar no habla coloquial o intuitivamente; habla con precisión y solo pretende usar términos que estén bien definidos. En el caso de "declaración", el estándar define el término; y cuando usa el término, usa su propia definición.
Al leer la especificación, puede aprender exactamente qué es una "declaración" y qué es exactamente lo que no es. Dicho de otra manera, el estándar describe el lenguaje "C". No describe "algún lenguaje que no sea C". En lo que respecta al estándar, "C con extensiones" es solo "algún lenguaje que no es C".
Por lo tanto, desde el punto de vista del estándar, la línea R no es una declaración en absoluto . ¡Ni siquiera analiza! También podría leer:
long long long __Foo_e!r!r!n!o()blurfl??/**
En lo que respecta a la especificación, esta es una "declaración" tanto como la línea R; es decir, no del todo.
Entonces, cuando dice la especificación C11, en la sección 6.5.3.2:
El operando del operador unario será un designador de función, el resultado de un operador
[]
o unario*
, o un valor l que designa un objeto que no es un campo de bits y no se declara con el especificador de clase de almacenamiento de registro .
... significa algo muy preciso que no se refiere a nada como Line R.
Ahora, considere la declaración del objeto int
al que se refiere errno
. (Nota: no me refiero a la declaración del nombre errno
, ya que, por supuesto, no podría haber tal declaración si errno
es, por ejemplo, una macro. Me refiero a la declaración del objeto int
subyacente).
El lenguaje anterior dice que puede tomar la dirección de un lvalue a menos que designe un campo de bit o designe un register
objeto "declarado". Y la especificación para el objeto errno
subyacente dice que es un valor lval modificable con duración local de subproceso.
Ahora bien, es cierto que la especificación no dice que el objeto errno
subyacente deba declararse en absoluto. Tal vez solo aparece a través de alguna magia de compilación definida por la implementación. Pero nuevamente, cuando la especificación dice "declarado con el especificador de clase de almacenamiento de registro", está utilizando su propia terminología.
Entonces, o bien el objeto errno
subyacente es "declarado" en el sentido estándar, en cuyo caso no puede ser tanto de register
como de subproceso local; o no está declarado en absoluto , en cuyo caso no está declarado como register
. De cualquier manera, dado que es un valor l, puede tomar su dirección.
(A menos que sea un campo de bits, pero creo que estamos de acuerdo en que un campo de bit no es un objeto de tipo int
.)
Podemos encontrar un contraejemplo: como un campo de bits podría tener un tipo int
, errno
puede ser un campo de bits. En ese caso, &errno
no sería válido. El comportamiento del estándar está aquí para no decir explícitamente que puede escribir &errno
, por lo que la definición del comportamiento indefinido se aplica aquí.
C11 (n1570), § 4. Conformidad
El comportamiento indefinido está indicado de otra manera en esta Norma Internacional por las palabras "comportamiento indefinido" u omisión de cualquier definición explícita de comportamiento.