c++ - quien - regulacion mexder
¿Por qué no se puede convertir la clase C++ 11 fuertemente tipada al tipo subyacente mediante un puntero? (6)
En C ++ 11 podemos lanzar una enumeración de tipo fuerte ( enum class
) a su tipo subyacente. Pero parece que no podemos lanzar un puntero a la misma:
enum class MyEnum : int {};
int main()
{
MyEnum me;
int iv = static_cast<int>(me); // works
int* ip = static_cast<int*>(&me); // "invalid static_cast"
}
Estoy tratando de entender por qué debería ser esto: ¿hay algo en el mecanismo de enumeración que hace que sea difícil o absurdo apoyar esto? ¿Es un simple descuido en la norma? ¿Algo más?
Me parece que si un tipo de enumeración se construye realmente sobre un tipo integral como anteriormente, deberíamos ser capaces de convertir no solo los valores sino también los punteros. Todavía podemos usar reinterpret_cast<int*>
o un reparto de estilo C pero es un martillo más grande de lo que pensé que necesitaríamos.
Una enumeración es un tipo distinto (3.9.2) con constantes nombradas. [...] Cada enumeración define un tipo que es diferente de todos los otros tipos. [...] Dos tipos de enumeración son compatibles con el diseño si tienen el mismo tipo subyacente.
[dcl.enum] (§7.2)
El tipo subyacente especifica el diseño de la enumeración en la memoria, no su relación con otros tipos en el sistema de tipos (como dice el estándar, es un tipo distinto , un tipo propio). Un puntero a una enum : int {}
nunca puede convertirse implícitamente en un int*
, de la misma manera que un puntero a una struct { int i; };
struct { int i; };
no se puede, aunque todos se parecen en la memoria.
Entonces, ¿por qué la conversión implícita a int
funciona en primer lugar?
Para una enumeración cuyo tipo subyacente es fijo, los valores de la enumeración son los valores del tipo subyacente. [...] El valor de un enumerador o un objeto de un tipo de enumeración sin ámbito se convierte en un número entero mediante una promoción integral (4.5).
[dcl.enum] (§7.2)
Por lo tanto, podemos asignar los valores de una enumeración a un int
porque son de tipo int
. Un objeto de tipo de enumeración se puede asignar a un int
debido a las reglas de la promoción de enteros. Por cierto, el estándar aquí señala específicamente que esto solo es cierto para las enumeraciones de estilo C (sin ámbito). Esto significa que aún necesita la static_cast<int>
en la primera línea de su ejemplo, pero tan pronto como convierta la enum class : int
en una enum : int
, funcionará sin la conversión explícita. Todavía no hay suerte con el tipo de puntero.
Las promociones integrales se definen en la norma en [conv.prom] (§4.5). Le ahorraré los detalles de citar la sección completa, pero el detalle importante aquí es que todas las reglas allí se aplican a los valores predefinidos de los tipos sin puntero, por lo que nada de esto se aplica a nuestro pequeño problema.
La pieza final del rompecabezas se puede encontrar en [expr.static.cast] (§5.2.9), que describe cómo funciona static_cast
.
Un valor de un tipo de enumeración con alcance (7.2) se puede convertir explícitamente en un tipo integral.
Eso explica por qué tu elenco de enum class
to int
funciona.
Pero tenga en cuenta que todos los static_cast
s permitidos en los tipos de punteros (de nuevo, no citaré la sección bastante extensa) requieren alguna relación entre los tipos. Si recuerda el principio de la respuesta, cada enumeración es un tipo distinto, por lo que no hay relación con su tipo subyacente u otras enumeraciones del mismo tipo subyacente.
Esto se relaciona con la respuesta de @ MarkB : la conversión estática de un puntero enum
a un puntero a int
es análogo a lanzar un puntero de un tipo integral a otro, incluso si ambos tienen el mismo diseño de memoria debajo y los valores de uno se convertirán implícitamente a static_cast
promociones integrales de las reglas, siguen siendo tipos no relacionados, por lo que static_cast
no funcionará aquí.
Creo que el error de pensar es que
enum class MyEnum : int {};
No es realmente la herencia. Por supuesto que puedes decir que MyEnum
es un int
. Sin embargo, es diferente de la herencia clásica, ya que no todas las operaciones que están disponibles en int
s también están disponibles para MyEnum
.
Comparemos esto con lo siguiente: Un círculo es una elipse. Sin embargo, casi siempre sería incorrecto implementar un CirlceShape
heredado de EllipseShape
ya que no todas las operaciones que son posibles en elipsis también son posibles para el círculo. Un ejemplo simple sería escalar la forma en la dirección x.
Por lo tanto, pensar en las clases de enumeración como heredadas de un tipo entero conduce a la confusión en su caso. No puede incrementar una instancia de una clase de enumeración, pero puede incrementar enteros. Dado que no es realmente una herencia, tiene sentido prohibir la difusión de punteros a estos tipos de manera estática. La siguiente línea no es segura:
++*reinterpret_cast<int*>(&me);
Esta podría ser la razón por la cual el comité prohibió static_cast
en este caso. En general, reinterpret_cast
se considera malo, mientras que static_cast
se considera aceptable.
Creo que el motivo de la primera static_cast
es poder trabajar con funciones y bibliotecas que esperan enum
estilo antiguo o incluso utilizan un grupo de valores definidos para las enumeraciones y esperan directamente un tipo integral. Pero no hay otra relación lógica entre el tipo enum
y un tipo integral, por lo que debería usar reinterpret_cast
si quiere ese lanzamiento. pero si tiene problemas con reinterpret_cast
, puede usar su propio ayudante:
template< class EnumT >
typename std::enable_if<
std::is_enum<EnumT>::value,
typename std::underlying_type<EnumT>::type*
>::type enum_as_pointer(EnumT& e)
{
return reinterpret_cast<typename std::underlying_type<EnumT>::type*>(&e);
}
o
template< class IntT, class EnumT >
IntT* static_enum_cast(EnumT* e,
typename std::enable_if<
std::is_enum<EnumT>::value &&
std::is_convertible<
typename std::underlying_type<EnumT>::type*,
IntT*
>::value
>::type** = nullptr)
{
return reinterpret_cast<typename std::underlying_type<EnumT>::type*>(&e);
}
Si bien esta respuesta puede no satisfacerle por la reason of prohibiting static_cast of enum pointers
, le ofrece una forma segura de usar reinterpret_cast
con ellos.
En su lugar, mirarlo de una manera ligeramente diferente. No puede static_cast
a long*
to int*
incluso si int
y long
tienen representaciones subyacentes idénticas. Por la misma razón, una enumeración basada en int
aún se trata como un tipo único, no relacionado con int
y como tal requiere reinterpret_cast
.
Las respuestas a sus preguntas se pueden encontrar en la sección 5.2.9 Reparto estático en el proyecto de norma.
Apoyo para permitir
int iv = static_cast<int>(me);
Se puede obtener de:
5.2.9 / 9 Un valor de un tipo de enumeración con alcance (7.2) se puede convertir explícitamente en un tipo integral. El valor no se modifica si el valor original puede representarse por el tipo especificado. De lo contrario, el valor resultante no se especifica.
Apoyo para permitir
me = static_cast<MyEnum>(100);
Se puede obtener de:
5.2.9 / 10 Un valor de tipo integral o de enumeración se puede convertir explícitamente a un tipo de enumeración. El valor no se modifica si el valor original está dentro del rango de los valores de enumeración (7.2). De lo contrario, el valor resultante no se especifica (y puede que no esté en ese rango).
Apoyo para no permitir
int* ip = static_cast<int*>(&me);
Se puede obtener de:
5.2.9 / 11 Un prvalor de tipo "puntero a cv1 B", donde B es un tipo de clase, se puede convertir a un prvalor de tipo "puntero a cv2 D", donde D es una clase derivada (Cláusula 10) de B , si existe una conversión estándar válida de "puntero a D" a "puntero a B" (4.10), cv2 es la misma calificación cv que, o mayor calificación cv que, cv1, y B no es una clase base virtual de D ni una clase base de una clase base virtual de D. El valor de puntero nulo (4.10) se convierte al valor de puntero nulo del tipo de destino. Si el prvalor de tipo "puntero a cv1 B" apunta a una B que es en realidad un subobjeto de un objeto de tipo D, el puntero resultante apunta al objeto que encierra de tipo D. De lo contrario, el resultado de la conversión no está definido.
static_cast
no se puede usar para convertir &me
a un int*
ya que MyEnum
y int
no están relacionados por herencia.
TL; DR: A los diseñadores de C ++ no les gusta el punning de tipo.
Otros han señalado por qué no está permitido por la norma; Trataré de explicar por qué los escritores de la norma podrían haberlo hecho de esa manera. De acuerdo con esta propuesta , la motivación principal para las enumeraciones fuertemente tipadas fue la seguridad de tipos. Desafortunadamente, la seguridad de tipo significa muchas cosas para muchas personas. Es justo asumir que la consistencia era otro objetivo del comité de estándares, así que examinemos la seguridad de tipos en otros contextos relevantes de C ++.
Seguridad tipo C ++
En C ++ en general, los tipos no están relacionados a menos que se especifique explícitamente que estén relacionados (a través de la herencia). Considera este ejemplo:
class A
{
double x;
int y;
};
class B
{
double x;
int y;
};
void foo(A* a)
{
B* b = static_cast<B*>(a); //error
}
Aunque A y B tienen la misma representación exacta (el estándar incluso los llamaría "tipos de diseño estándar"), no se puede convertir entre ellos sin un reinterpret_cast
. Del mismo modo, esto también es un error:
class C
{
public:
int x;
};
void foo(C* c)
{
int* intPtr = static_cast<int*>(c); //error
}
Aunque sabemos que lo único en C es un int y puedes acceder a él libremente, el static_cast
falla. ¿Por qué? No se especifica explícitamente que estos tipos están relacionados. C ++ se diseñó para admitir la programación orientada a objetos, que proporciona una distinción entre la composición y la herencia. Puede convertir entre los tipos relacionados por herencia, pero no los relacionados por composición.
Basado en el comportamiento que has visto, está claro que las enumeraciones fuertemente tipadas están relacionadas por composición con sus tipos subyacentes. ¿Por qué este podría haber sido el modelo que eligió el comité estándar?
Composición vs Herencia
Hay muchos artículos sobre este tema mejor escritos que cualquier otro que pueda encajar aquí, pero intentaré resumir. Cuando usar la composición y cuándo usar la herencia es ciertamente un área gris, pero hay muchos puntos a favor de la composición en este caso.
- Las enumeraciones de tipo fuerte no están diseñadas para ser utilizadas como valores integrales. Por lo tanto, la relación ''is-a'' indicada por herencia no encaja.
- En el nivel más alto, las enumeraciones están destinadas a representar un conjunto de valores discretos. El hecho de que esto se implemente mediante la asignación de un número de identificación a cada valor generalmente no es importante (desafortunadamente, C expone y, por lo tanto, hace cumplir esta relación).
- Mirando hacia atrás en la propuesta , la razón enumerada para permitir un tipo subyacente específico es especificar el tamaño y la firmeza de la enumeración. Esto es mucho más de un detalle de implementación que una parte esencial de la enumeración, favoreciendo nuevamente la composición.
Podría discutir durante días si la herencia o la composición son mejores en este caso, pero finalmente se tuvo que tomar una decisión y el comportamiento se basó en la composición.