¿Cuál es la mejor manera de implementar el idioma “nuevo tipo” en C++? [duplicate]

7 minutos de lectura

avatar de usuario
kenba

Desde que aprendí Rust, me he convertido en un fanático de la idioma de nuevo tipo que deduzco que Rust tomó prestado de Haskell.

Un newtype es un tipo distinto basado en un tipo estándar que garantiza que los parámetros de la función sean del tipo correcto.
por ejemplo, el old_enough La función a continuación debe pasar una edad en años. No se compilará con una edad en Días o como un simple i64.

struct Days(i64);
struct Years(i64);

fn old_enough(age: &Years) -> bool {
    age.0 >= 18
}

Esto es diferente de un typedef o using declaración en C++ que simplemente cambia el nombre del tipo.
por ejemplo el old_enough La función a continuación aceptaría una intuna edad en Dayso cualquier otra cosa que se convierta en un int:

typedef int Days;
using Years = int;

bool old_enough(Years age) {
    return age >= 18;
}

Como el ejemplo anterior solo usa números enteros, este publicar en Reddit sugiere usar clases de enumeración, por ejemplo:

enum class Days : int {};
enum class Years : int {};

bool old_enough(Years age) {
    return static_cast<int>(age) >= 18;
}

O simplemente podría usar estructuras, como Rust, por ejemplo:

struct Days final {int value;};
struct Years final {int value;};

bool old_enough(Years age) {
    return age.value >= 18;
}

¿Cuál es la mejor manera de implementar el newtype idioma en C++?
¿Existe un método estándar?

EDITAR la pregunta Strongly typed using y typedef es similar. Sin embargo, no considera la newtype modismo.

  • En C++ probablemente harías un Year clase con validación a nivel de constructor y/o asignación. Me gusta auto year = new Year(2002) después year.old_enough()o para una implementación más estricta, throw cuando se proporciona un valor no válido.

    – tadman

    26 de junio de 2020 a las 7:47


  • si realmente solo necesita el mismo tipo pero como un tipo diferente, lamentablemente requiere bastante repetitivo. Afortunadamente alguien ya lo escribió, puedes usar BOOST_STRONG_TYPEDEFpero en general estoy de acuerdo con tad, un Year no es un int y merece su propio tipo

    – 463035818_no_es_un_número

    26 de junio de 2020 a las 7:48


  • ¿Responde esto a tu pregunta? Fuertemente tipado usando y typedef

    – subrayado_d

    26 de junio de 2020 a las 9:55

  • @tadman ¿Quieres decir? auto year = Year(2002);

    – usuario253751

    26 de junio de 2020 a las 13:46

  • Podría valer la pena señalar que para este ejemplo en particular, la biblioteca estándar de C++ ya tiene std::chrono::years y std::chrono::days que ya son esencialmente este tipo de envoltorio. (Aunque en este caso, también hay conversiones implícitas posibles de, por ejemplo, std::chrono::days a std::chrono::seconds lo que en segundo plano multiplicará el valor por el factor de conversión correcto). Boost también tiene una biblioteca de unidades que maneja envolver valores dobles en SI (o unidades imperiales) y hacer conversiones en segundo plano.

    – Daniel Schepler

    27 de junio de 2020 a las 1:15

avatar de usuario
dfrib

¿Cuál es la mejor manera de implementar el newtype modismo en C++?

Clasificación lo mejor muchas veces terminan en el dominio preferencial, pero ya mencionaste dos enfoques alternativos: simplemente estructuras personalizadas que envuelven un valor de un tipo común (digamos int), o utilizando enum clases con un tipo subyacente explícitamente especificado para tipos casi idénticos fuertemente tipados.

Si buscas principalmente alias de tipo fuertemente tipados de un tipo común, digamos

struct Number { int value; }

o, un tipo común con un tipo subyacente parametrizable

template<typename ValueType = int>
struct Number { ValueType value; }

después otro El enfoque común (que también facilita la reutilización de la funcionalidad entre tipos fuertemente distintos pero relacionados) es hacer (/ expandir) el Number clase (plantilla) una plantilla de clase parametrizada sobre plantilla de tipo etiqueta parámetro, de modo que las especializaciones sobre los tipos de etiquetas dan como resultado una tipificación fuerte. Como señaló @Matthieu M., podemos declarar una estructura como parte de la lista de argumentos de la plantilla para una especialización dada, lo que permite una declaración de etiqueta liviana y el etiquetado de alias en una sola declaración de alias:

template<typename Tag, typename ValueType = int>
struct Number {
    ValueType value;
    // ... common number functionality.
};

using YearNumber = Number<struct NumberTag>;
using DayNumber = Number<struct DayTag>;

void takeYears(const YearNumber&) {}
void takeDays(const DayNumber&) {}

int main() {
    YearNumber y{2020};
    DayNumber d{5};
    
    takeYears(y);
    //takeDays(y);  // error: candidate function not viable
    
    takeDays(d);
    //takeYears(d);  // error: candidate function not viable
    
    return 0;
}

Tenga en cuenta que en caso de que desee especializar funciones de no miembros de la Number plantilla de clase para etiquetas específicas (o, por ejemplo, usar el envío de etiquetas para un propósito similar), deberá declarar las etiquetas de tipo fuera de la declaración de alias.

  • Puedes ir más ligero y evitar el enum aprovechando las declaraciones de estructura “en línea”: using YearNumber = Number<struct YearTag>;

    – Matthieu M.

    26 de junio de 2020 a las 9:40

  • @MatthieuM. Eso es muy bueno, no sabía que declaras estructuras como un argumento de plantilla de tipo en la lista de argumentos de plantilla. Gracias, actualizaré la respuesta en breve.

    – dfrib

    26 de junio de 2020 a las 9:41


  • @MatthieuM. Supongo que una limitación con ese enfoque es en caso de que desee, por ejemplo, especializar alguna función miembro que no sea de plantilla del Number plantilla de clase basada en su etiqueta, y/o delegar algunas funciones de miembros que no son de plantilla a funciones de miembros personalizadas concretas (privadas) a través del envío de etiquetas. Sin embargo, se adapta fácilmente si surgiera ese caso de uso.

    – dfrib

    26 de junio de 2020 a las 9:58

  • @underscore_d Afaik anónimo. Las estructuras son, por requerimiento, siempre definido en sus declaraciones. IIRC GT21/RD 62 se refirió a esto (“Los siguientes tipos no se utilizarán como argumento de plantilla para un parámetro de tipo de plantilla: … una clase sin nombre o tipo de enumeración que no tiene nombre para fines de vinculación”). no puedo encontrar, de [temp.arg.type]sin embargo, que un especificador de clase es un valido argumento de plantilla, pero esto aparentemente funciona (con todos los compiladores que he probado). ¿Pensamientos?

    – dfrib

    26 de junio de 2020 a las 10:12

  • @underscore_d Como no pude encontrar las declaraciones de clase estándar que cubren (incompletas) en un argumento de plantilla para un parámetro de plantilla que no es de tipo, publiqué una pregunta al respecto, para que los abogados con más experiencia en idiomas la prueben.

    – dfrib

    26 de junio de 2020 a las 10:54

He usado boost strong typedef en el pasado. los documentación en parece bastante escaso, pero fwiw, parece ser utilizado por Facebook, y LLVM parece tener un cosa parecida llamada LLVM_YAML_STRONG_TYPEDEFlo que indica que puede haber tenido alguna exposición del mundo real.

avatar de usuario
Darune

Si tiene impulso, BOOST_STRONG_TYPEDEF hace exactamente lo que quiere, como ya se vio en esta respuesta.


No hay nada en el lenguaje C++ (todavía) que pueda hacerlo directamente como quieras. Pero, de nuevo, las necesidades detalladas podrían ser diferentes, por ejemplo. alguien podría decir que está bien hacer una construcción implícita mientras que otro podría decir que tiene que ser explícito. Por eso y otros combinaciones1 es difícil proporcionar un mecanismo que satisfaga a todos y ya tenemos un alias de tipo normal (es decir, using, que ofc. es diferente de un definición de tipo fuerte).

Habiendo dicho eso, C++ le brinda suficientes herramientas para que pueda construir esta herramienta genérica usted mismo y no es completamente difícil de hacer si tiene algo de experiencia con plantillas, etc.

al final depende de que nuevo tipo problemas que realmente tiene, por ejemplo. ¿Solo necesitas un puñado o los vas a hacer a granel? Para algo ordinario como Años y Días, podría usar estructuras simples:

struct Days {int value;};

struct Years {int value;};

Sin embargo, si debe evitar una situación como esta:

bool isold(Years y);

...

isold({5});

Luego debe hacer un constructor y hacerlo explícito, es decir:

struct Years {
   explicit Years(int i);
...

1 otra combinación podría ser, por ejemplo, si se debe permitir que el nuevo tipo se convierta al tipo subyacente, podría ser útil para algo como into podría ser peligroso dependiendo del contexto

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad