tipados tipado sinonimo qué que lenguajes lenguaje fuertemente fuerte estatico débilmente dinamicos dinamico definicion c++ api types strong-typing

c++ - qué - tipado sinonimo



¿Qué tan lejos llegar con un lenguaje fuertemente tipado? (14)

Digamos que estoy escribiendo una API, y una de mis funciones toma un parámetro que representa un canal, y solo estará entre los valores 0 y 15. Podría escribirlo así:

void Func(unsigned char channel) { if(channel < 0 || channel > 15) { // throw some exception } // do something }

O aprovecho que C ++ es un lenguaje fuertemente tipado y me hago un tipo:

class CChannel { public: CChannel(unsigned char value) : m_Value(value) { if(channel < 0 || channel > 15) { // throw some exception } } operator unsigned char() { return m_Value; } private: unsigned char m_Value; }

Mi función ahora se convierte en esto:

void Func(const CChannel &channel) { // No input checking required // do something }

Pero, ¿es esto un exceso total? Me gusta la auto-documentación y la garantía de que es lo que dice que es, pero ¿vale la pena pagar la construcción y destrucción de tal objeto, y mucho menos todo el tipado adicional? Por favor, hágame saber sus comentarios y alternativas.


¡Esto es abstracción, amigo! Siempre es mejor trabajar con objetos


El ejemplo del canal es difícil:

  • Al principio parece un tipo de entero simple de rango limitado, como el que se encuentra en Pascal y Ada. C ++ no te da ninguna manera de decir esto, pero una enumeración es lo suficientemente buena.

  • Si miras más de cerca, ¿podría ser una de esas decisiones de diseño que probablemente cambiarán? ¿Podrías empezar a referirte a "canal" por frecuencia? Por letras de llamada (WGBH, entra) Por la red?

Mucho depende de tus planes. ¿Cuál es el objetivo principal de la API? ¿Cuál es el modelo de costo? ¿Se crearán canales con mucha frecuencia (sospecho que no)?

Para obtener una perspectiva ligeramente diferente, veamos el costo de arruinar:

  • Expones el representante como int . Los clientes escriben una gran cantidad de código, la interfaz es respetada o su biblioteca se detiene con una falla de aserción. Crear canales es muy barato. Pero si necesita cambiar la forma en que hace las cosas, pierde "compatibilidad con errores hacia atrás" y molesta a los autores de clientes descuidados.

  • Lo mantienes abstracto Todo el mundo tiene que usar la abstracción (no tan mal), y todos están preparados para el futuro contra los cambios en la API. Mantener la compatibilidad con versiones anteriores es pan comido. Pero la creación de canales es más costosa y, lo que es peor, la API debe indicar cuidadosamente cuándo es seguro destruir un canal y quién es responsable de la decisión y la destrucción. El peor escenario es que crear / destruir canales conduce a una gran fuga de memoria u otro fallo de rendimiento, en cuyo caso recurre a la enumeración.

Soy un programador descuidado, y si fuera por mi propio trabajo, iría con la enumeración y me comería el costo si la decisión de diseño cambia más adelante. Pero si esta API fuera para muchos otros programadores como clientes, utilizaría la abstracción.

Evidentemente soy un relativista moral.


En mi opinión, no creo que lo que estás proponiendo sea una gran sobrecarga, pero para mí, prefiero guardar el tipeo y simplemente poner en la documentación que todo lo que esté fuera de 0..15 no esté definido y usar un assert () en la función para detectar errores para compilaciones de depuración. No creo que la complejidad agregada ofrezca mucha más protección para los programadores que ya están acostumbrados a la programación del lenguaje C ++ que contiene muchos comportamientos indefinidos en sus especificaciones.


No puedo creer que nadie haya mencionado enum hasta ahora. No le dará una protección a prueba de balas, pero aún mejor que un tipo de datos entero simple.


No, no es excesivo; siempre debes tratar de representar las abstracciones como clases. Hay un trillón de razones para hacer esto y la sobrecarga es mínima. Sin embargo, llamaría a la clase Channel, no a CChannel.


Sí, la idea vale la pena, pero (IMO) escribir una clase completa y separada para cada rango de enteros es algo sin sentido. Me he topado con suficientes situaciones que requieren enteros de rango limitado que he escrito una plantilla para este propósito:

template <class T, T lower, T upper> class bounded { T val; void assure_range(T v) { if ( v < lower || upper <= v) throw std::range_error("Value out of range"); } public: bounded &operator=(T v) { assure_range(v); val = v; return *this; } bounded(T const &v=T()) { assure_range(v); val = v; } operator T() { return val; } };

Usarlo sería algo así como:

bounded<unsigned, 0, 16> channel;

Por supuesto, puede ser más elaborado que esto, pero este sencillo todavía maneja bastante bien el 90% de las situaciones.


Si agrega constantes para los 16 canales diferentes, y también un método estático que busca el canal para un valor determinado (o arroja una excepción si está fuera de rango), entonces esto puede funcionar sin ninguna sobrecarga adicional de creación de objetos por llamada de método.

Sin saber cómo se usará este código, es difícil decir si es excesivo o no o agradable de usar. Pruébelo usted mismo, escriba algunos casos de prueba usando ambos enfoques de un char y una clase segura, y vea cuál le gusta. Si te cansas de ello después de escribir algunos casos de prueba, probablemente sea mejor evitarlo, pero si te gusta el enfoque, entonces podría ser un guardián.

Si se trata de una API que va a ser utilizada por muchos, tal vez abrirla para una revisión podría darte valiosos comentarios, ya que supuestamente conocen bastante bien el dominio de la API.


Si algo es excesivo o no a menudo depende de muchos factores diferentes. Lo que podría ser excesivo en una situación podría no serlo en otra.

Este caso podría no ser exagerado si tenía muchas funciones diferentes que todos los canales aceptados y todos tenían que hacer la misma verificación de rango. La clase Channel evitaría la duplicación de código y también mejoraría la legibilidad de las funciones (como nombrar a la clase Channel en lugar de CChannel - Neil B. está en lo cierto).

A veces, cuando el rango es lo suficientemente pequeño, en su lugar definiré una enumeración para la entrada.


Si quisiera este enfoque más simple, generalícelo para que pueda sacarle más provecho, en lugar de adaptarlo a algo específico. Entonces la pregunta no es "¿Debería hacer una clase completamente nueva para esta cosa específica?" pero "¿debería usar mis utilidades?"; el último es siempre sí. Y los servicios públicos siempre son útiles.

Entonces haz algo como:

template <typename T> void check_range(const T& pX, const T& pMin, const T& pMax) { if (pX < pMin || pX > pMax) throw std::out_of_range("check_range failed"); // or something else }

Ahora ya tienes esta buena utilidad para verificar los rangos. Su código, incluso sin el tipo de canal, ya puede hacerse más limpio al usarlo. Puedes ir más allá:

template <typename T, T Min, T Max> class ranged_value { public: typedef T value_type; static const value_type minimum = Min; static const value_type maximum = Max; ranged_value(const value_type& pValue = value_type()) : mValue(pValue) { check_range(mValue, minimum, maximum); } const value_type& value(void) const { return mValue; } // arguably dangerous operator const value_type&(void) const { return mValue; } private: value_type mValue; };

Ahora tiene una buena utilidad, y puede hacer:

typedef ranged_value<unsigned char, 0, 15> channel; void foo(const channel& pChannel);

Y es reutilizable en otros escenarios. Simplemente pegue todo en un archivo "checked_ranges.hpp" y "checked_ranges.hpp" cuando lo necesite. Nunca es malo hacer abstracciones, y tener utilidades a su alrededor no es dañino.

Además, nunca te preocupes por los gastos generales. Crear una clase simplemente consiste en ejecutar el mismo código que harías de todos modos. Además, el código limpio es preferible a cualquier otra cosa; el rendimiento es una última preocupación. Una vez que haya terminado, puede obtener un generador de perfiles para medir (sin adivinar) dónde están las partes lentas.


Tienes que hacer una elección. No hay una bala de plata aquí.

Actuación

Desde la perspectiva del rendimiento, la sobrecarga no va a ser mucho o nada. (a menos que tenga que contar los ciclos de CPU) Por lo tanto, probablemente este no sea el factor determinante.

Simplicidad / facilidad de uso, etc.

Haz que la API sea simple y fácil de entender / aprender. Debes saber / decidir si números / enums / clase sería más fácil para el usuario de API

Mantenibilidad

  1. Si está seguro de que el tipo de canal va a ser un número entero en el futuro previsible, me iría sin la abstracción (considere usar enumeraciones)

  2. Si tiene muchos casos de uso para valores acotados, considere usar las plantillas (Jerry)

  3. Si crees, Channel puede tener potencialmente métodos para convertirlo en una clase en este momento.

Esfuerzo de codificación Es una cosa de una vez. Entonces, siempre piense en el mantenimiento.


Un entero con valores solo entre 0 y 15 es un entero de 4 bits sin signo (o medio byte, nibble. Imagino que si esta lógica de conmutación de canal se implementara en hardware, entonces el número de canal podría representarse así, un 4 -bit registro). Si C ++ tuviera eso como tipo, lo harías allí mismo:

void Func(unsigned nibble channel) { // do something }

Por desgracia, desafortunadamente no. Puede relajar la especificación API para expresar que el número de canal se da como un carácter sin signo, con el canal real que se calcula utilizando una operación de módulo 16:

void Func(unsigned char channel) { channel &= 0x0f; // truncate // do something }

O bien, use un campo de bits:

#include <iostream> struct Channel { // 4-bit unsigned field unsigned int n : 4; }; void Func(Channel channel) { // do something with channel.n } int main() { Channel channel = {9}; std::cout << "channel is" << channel.n << ''/n''; Func (channel); }

Este último podría ser menos eficiente.


Voto por su primer acercamiento, porque es más simple y fácil de entender, mantener y extender, y porque es más probable que se asigne directamente a otros idiomas si su API tiene que volverse a implementar / traducir / portar / etc.


Ya sea que arroje una excepción al construir su objeto "CChannel" o en la entrada al método que requiere la restricción, no hay mucha diferencia. En cualquier caso, estás haciendo aserciones en tiempo de ejecución, lo que significa que el sistema de tipos no te está haciendo ningún bien, ¿o sí?

Si desea saber qué tan lejos puede llegar con un lenguaje fuertemente tipado, la respuesta es "muy lejos, pero no con C ++". El tipo de potencia que necesita para aplicar estáticamente una restricción como "este método solo se puede invocar con un número entre 0 y 15" requiere algo llamado tipos dependientes , es decir, tipos que dependen de los valores .

Para poner el concepto en sintaxis pseudo-C ++ (pretendiendo que C ++ tenía tipos dependientes), podría escribir esto:

void Func(unsigned char channel, IsBetween<0, channel, 15> proof) { ... }

Tenga en cuenta que IsBetween está parametrizado por valores en lugar de por tipos . Para llamar ahora a esta función en su programa, debe proporcionar al compilador el segundo argumento, proof , que debe tener el tipo IsBetween<0, channel, 15> . Lo que quiere decir que tienes que demostrar en tiempo de compilación que el channel está entre 0 y 15. Esta idea de tipos que representan proposiciones, cuyos valores son pruebas de esas proposiciones, se llama Corrigión de Curry-Howard .

Por supuesto, probar tales cosas puede ser difícil. Dependiendo del dominio de su problema, la relación costo / beneficio puede inclinarse fácilmente a favor de simplemente aplicar controles de tiempo de ejecución en su código.


Parece exagerado, especialmente el acceso operator unsigned char() del operator unsigned char() . No estás encapsulando datos, estás evidenciando cosas más complicadas y, probablemente, más propensas a errores.

Los tipos de datos como su Channel generalmente son parte de algo más abstracto.

Entonces, si usa ese tipo en su clase ChannelSwitcher , podría usar typedef comentado directamente en el cuerpo de ChannelSwitcher (y, probablemente, su typedef será public ).

// Currently used channel type typedef unsigned char Channel;