anlyticsed - El concepto de `Nil` en C++
all minecraft title screen messages (6)
Recuerdas de tus clases de pregrado en algoritmos que es muy útil tener un concepto de Nil
, con el que cualquier cosa se puede asignar o comparar. (Por cierto, nunca hice una licenciatura en informática). En Python podemos usar None
; en Scala no hay Nothing
(que es un subobjeto de todo si lo estoy entendiendo correctamente). Pero mi pregunta es, ¿cómo podemos tener Nil
en C ++? A continuación están mis pensamientos.
Podríamos definir un objeto singleton utilizando el patrón de diseño de Singleton , pero mi impresión actual es que la mayoría de ustedes se estremecería al pensarlo.
O podríamos definir un global o un estático.
Mi problema es que en ninguno de estos casos, no puedo pensar en una manera de poder asignar cualquier variable de cualquier tipo a Nil
, o ser capaz de comparar cualquier objeto de cualquier tipo con Nil
. Python''s None
es útil porque Python está tipeado dinámicamente; La Nothing
de Scala (que no debe confundirse con Scala Nil
, que significa lista vacía) lo resuelve elegantemente porque Nothing
es un subobjeto de todo. Entonces, ¿hay una manera elegante de tener Nil
en C ++?
C ++ sigue el principio de que no pagas por lo que no usas. Por ejemplo, si quiere un entero de 32 bits para almacenar el rango completo de valores de 32 bits y un indicador adicional sobre si es Nil, esto requerirá más de 32 bits de almacenamiento. Ciertamente, puede crear clases inteligentes para representar este comportamiento, pero no está disponible "de fábrica".
En C ++ 11 hay un valor nullptr que se puede asignar a cualquier tipo de puntero.
Si no es un tipo de puntero, debe estar allí. Para valores particulares, podría definir valores especiales de "no puedo existir", pero no hay una solución universal.
Si no existe, tus elecciones son:
- señalarlo y usar nullptr indicar un valor inexistente, o
- mantén una bandera por separado para indicar existencia o inexistencia (que es lo que
boost::optional
hace por ti), o - defina un valor especial para este uso particular para representar un valor inexistente (tenga en cuenta que esto no siempre es posible).
En la mayoría de esos idiomas, las variables no se implementan como el nombre de los objetos, sino como manejadores de los objetos.
Dichos controles pueden configurarse para "no contener nada" con muy poca carga adicional. Esto es análogo a un puntero C ++, donde el estado nullptr
corresponde a "señalar a nada".
A menudo, dichos idiomas se llenan completamente en la recolección de basura. Tanto la recolección de basura como la referencia indirecta forzada de datos tienen importantes resultados de rendimiento en la práctica, y restringen el tipo de operaciones que puede hacer.
Cuando utiliza una variable en dichos idiomas, primero tiene que ''seguir el puntero'' para llegar al objeto real.
En C ++, un nombre de variable típicamente se refiere al objeto real en cuestión, no una referencia a él. Cuando usa una variable en C ++, accede directamente a ella. Un estado adicional (que corresponde a "nada") requeriría una sobrecarga adicional en cada variable, y uno de los principios de C ++ es que no pagas por lo que usas.
Hay formas de hacer un tipo de datos "nullable" en C ++. Estos varían desde el uso de punteros crudos, el uso de punteros inteligentes o el uso de algo como std::experimantal::optional
. Todos tienen un estado para "no hay nada allí", generalmente detectable al tomar el objeto y evaluarlo en un contexto bool
. Para acceder a los datos reales (suponiendo que existan), usa unario *
o ->
.
No puede tener esto en C ++ sin tener que depender de un contenedor que encapsule su tipo (como boost::optional
).
De hecho, hay una combinación de razones, ya que C ++ es:
- lenguaje tipado estáticamente: una vez que define una variable con un tipo, se adhiere a ese tipo (lo que significa que no puede asignar un valor
None
que no está entre lo que cada tipo puede representar, lo que podría hacer en otros idiomas dinámicamente tipados) como Python) - idioma con tipos incorporados [1] y ningún objeto base común para sus tipos: no puede tener la semántica de
None
más "Todos los valores que este tipo podría representar de lo contrario" en un objeto común para todos sus tipos.
[1] que no son como los tipos incorporados de Java que tienen su clase heredando de java.lang.Object
.
Hay dos conceptos relacionados que describe como Nil
: el tipo de unidad y el tipo de opción .
Tipo de unidad
Esto es lo que NoneType
es en Python y nullptr_t
está en C ++, es solo un tipo especial que tiene un único valor que transmite este comportamiento específico. Dado que Python está tipeado dinámicamente, cualquier objeto puede compararse con None
pero en C ++ solo podemos hacer esto para los punteros:
void foo(T* obj) {
if (obj == nullptr) {
// Nil
}
}
Esto es semánticamente idéntico al de Python:
def foo(obj):
if foo is None:
# Nil
Tipo de opción
Python no tiene (ni necesita) tal característica, pero sí toda la familia ML. Esto es implementable en C ++ a través de boost::optional
. Esta es una encapsulación de tipo seguro de la idea de que un objeto en particular puede tener un valor, o no. Sin embargo, esta idea es más expresiva en la familia funcional que en C ++:
def foo(obj: Option[T]) = obj match {
case None => // "Nil" case
case Some(v) => // Actual value case
}
Aunque es bastante fácil de entender también en C ++, una vez que lo ves:
void foo(boost::optional<T> const& obj) {
if (obj) {
T const& value = *obj;
// etc.
}
else {
// Nil
}
}
La ventaja aquí es que el tipo de opción es un tipo de valor, y puede expresar fácilmente un "valor" de nada (por ejemplo, su optional<int*>
puede almacenar nullptr
como un valor, por separado de "Nada"). Además, esto puede funcionar con cualquier tipo, no solo con punteros; simplemente tiene que pagar por la funcionalidad adicional. Una optional<T>
será más grande que una T
y será más costosa de usar (aunque solo por un poco, aunque eso poco podría importar mucho).
A C ++ Nil
Podemos combinar esas ideas para hacer un Nil
en C ++, al menos uno que funcione con punteros y opcionales.
struct Nil_t { };
constexpr Nil_t Nil;
// for pointers
template <typename T>
bool operator==(const T* t, Nil_t ) { return t == nullptr; }
template <typename T>
bool operator==(Nil_t, const T* t ) { return t == nullptr; }
// for optionals, rhs omitted for brevity
template <typename T>
bool operator==(const boost::optional<T>& t, Nil_t ) { return !t; }
(Tenga en cuenta que incluso podemos generalizar esto a cualquier cosa que implemente el operator!
template <typename T>
bool operator==(const T& t, Nil_t ) { return !t; }
, pero sería mejor limitarnos a los casos claros y me gusta lo explícito que te dan los punteros y las opciones)
Así:
int* i = 0;
std::cout << (i == Nil); // true
i = new int(42);
std::cout << (i == Nil); // false
No, no existe el valor nil
universal en C ++. Verbatim, los tipos de C ++ no están relacionados y no comparten valores comunes.
Puede lograr alguna forma de valores compartibles mediante el uso de herencia, pero debe hacerlo explícitamente, y solo puede hacerse para los tipos definidos por el usuario.