Plantillas de C++ que aceptan solo ciertos tipos

14 minutos de lectura

En Java, puede definir una clase genérica que acepte solo tipos que amplíen la clase de su elección, por ejemplo:

public class ObservableList<T extends List> {
  ...
}

Esto se hace usando la palabra clave “extiende”.

¿Hay algún equivalente simple a esta palabra clave en C++?

  • Ya es una pregunta bastante antigua … Siento que lo que falta aquí (también de las respuestas) es que los genéricos de Java no son realmente un equivalente de las plantillas en C ++. Hay similitudes, pero en mi humilde opinión, se debe tener cuidado al traducir directamente una solución de Java a C++ solo para darse cuenta de que tal vez estén hechas para diferentes tipos de problemas;)

    – 463035818_no_es_un_número

    21 de agosto de 2018 a las 11:48

Plantillas de C++ que aceptan solo ciertos tipos
Rapptz

Esto generalmente no está justificado en C ++, como han señalado otras respuestas aquí. En C++, tendemos a definir tipos genéricos en función de otras restricciones distintas de “hereda de esta clase”. Si realmente quería hacer eso, es muy fácil hacerlo en C++11 y <type_traits>:

#include <type_traits>

template<typename T>
class observable_list {
    static_assert(std::is_base_of<list, T>::value, "T must inherit from list");
    // code here..
};

Sin embargo, esto rompe muchos de los conceptos que la gente espera en C++. Es mejor usar trucos como definir tus propios rasgos. Por ejemplo, tal vez observable_list quiere aceptar cualquier tipo de contenedor que tenga los typedefs const_iterator y un begin y end función miembro que devuelve const_iterator. Si restringe esto a las clases que heredan de list entonces un usuario que tiene su propio tipo que no hereda de list pero proporciona estas funciones miembro y typedefs no podría usar su observable_list.

Hay dos soluciones a este problema, una de ellas es no restringir nada y confiar en el tipo de pato. Una gran desventaja de esta solución es que implica una gran cantidad de errores que pueden ser difíciles de asimilar para los usuarios. Otra solución es definir rasgos para restringir el tipo proporcionado para cumplir con los requisitos de la interfaz. La gran desventaja de esta solución es que implica una escritura adicional que puede verse como molesta. Sin embargo, el lado positivo es que podrá escribir sus propios mensajes de error a la static_assert.

Para completar, se da la solución al ejemplo anterior:

#include <type_traits>

template<typename...>
struct void_ {
    using type = void;
};

template<typename... Args>
using Void = typename void_<Args...>::type;

template<typename T, typename = void>
struct has_const_iterator : std::false_type {};

template<typename T>
struct has_const_iterator<T, Void<typename T::const_iterator>> : std::true_type {};

struct has_begin_end_impl {
    template<typename T, typename Begin = decltype(std::declval<const T&>().begin()),
                         typename End   = decltype(std::declval<const T&>().end())>
    static std::true_type test(int);
    template<typename...>
    static std::false_type test(...);
};

template<typename T>
struct has_begin_end : decltype(has_begin_end_impl::test<T>(0)) {};

template<typename T>
class observable_list {
    static_assert(has_const_iterator<T>::value, "Must have a const_iterator typedef");
    static_assert(has_begin_end<T>::value, "Must have begin and end member functions");
    // code here...
};

Hay muchos conceptos que se muestran en el ejemplo anterior que muestran las características de C++11. Algunos términos de búsqueda para los curiosos son plantillas variádicas, SFINAE, expresión SFINAE y rasgos de tipo.

  • Nunca me di cuenta de que las plantillas de C ++ usan la escritura pato hasta hoy. ¡Un poco extraño!

    – Andy

    9 de junio de 2015 a las 13:36

  • Dadas las amplias restricciones políticas C++ introducido a Cno estoy seguro de por qué template<class T:list> es un concepto tan ofensivo. Gracias por el consejo.

    – bvj

    24 de mayo de 2017 a las 5:42


  • Si alguien se pregunta qué es template<typename... Args>: es.cppreference.com/w/cpp/language/parameter_pack

    – zardosht

    23 de agosto de 2020 a las 21:52


Plantillas de C++ que aceptan solo ciertos tipos
j_random_hacker

Sugiero usar Boost afirmación estática característica en concierto con is_base_of de la biblioteca de Rasgos de Tipo de Impulso:

template<typename T>
class ObservableList {
    BOOST_STATIC_ASSERT((is_base_of<List, T>::value)); //Yes, the double parentheses are needed, otherwise the comma will be seen as macro argument separator
    ...
};

En algunos otros casos más simples, puede simplemente declarar una plantilla global, pero solo definirla (especializarla explícita o parcialmente) para los tipos válidos:

template<typename T> class my_template;     // Declare, but don't define

// int is a valid type
template<> class my_template<int> {
    ...
};

// All pointer types are valid
template<typename T> class my_template<T*> {
    ...
};

// All other types are invalid, and will cause linker error messages.

[Minor EDIT 6/12/2013: Using a declared-but-not-defined template will result in linker, not compiler, error messages.]

  • Las afirmaciones estáticas también son buenas. 🙂

    – macbirdie

    17 de mayo de 2009 a las 10:46

  • @John: Me temo que la especialización solo coincidiría myBaseType exactamente. Antes de descartar Boost, debe saber que la mayor parte es código de plantilla de solo encabezado, por lo que no hay costo de memoria o tiempo en el tiempo de ejecución para las cosas que no usa. También las cosas particulares que estarías usando aquí (BOOST_STATIC_ASSERT() y is_base_of<>) se puede implementar usando solo declaraciones (es decir, no definiciones de funciones o variables) por lo que tampoco ocuparán espacio ni tiempo.

    – j_random_hacker

    11 de diciembre de 2011 a las 23:20

  • Ha llegado C++11. Ahora podemos usar static_assert(std::is_base_of<List, T>::value, "T must extend list").

    –Siyuan Ren

    10 de septiembre de 2013 a las 2:55


  • Por cierto, la razón por la que el doble paréntesis es necesario es porque BOOST_STATIC_ASSERT es una macro y el paréntesis adicional evita que el preprocesador interprete la coma dentro de los argumentos de la función is_base_of como un segundo argumento de macro.

    – jfritz42

    13 de agosto de 2015 a las 19:29

  • @Andreyua: Realmente no entiendo lo que falta. Podrías intentar declarar una variable my_template<int> x; o my_template<float**> y; y verifique que el compilador los permita, y luego declare una variable my_template<char> z; y verifica que no.

    – j_random_hacker

    16 de noviembre de 2017 a las 16:20


Plantillas de C++ que aceptan solo ciertos tipos
jalf

La solución simple, que nadie ha mencionado todavía, es simplemente ignorar el problema. Si trato de usar un int como un tipo de plantilla en una plantilla de función que espera una clase de contenedor como vector o lista, obtendré un error de compilación. Crudo y simple, pero resuelve el problema. El compilador intentará usar el tipo que especifique y, si falla, generará un error de compilación.

El único problema con eso es que los mensajes de error que reciba serán difíciles de leer. Sin embargo, es una forma muy común de hacer esto. La biblioteca estándar está llena de plantillas de funciones o clases que esperan cierto comportamiento del tipo de plantilla y no hacen nada para comprobar que los tipos utilizados son válidos.

Si desea mensajes de error más agradables (o si desea detectar casos que no producirían un error del compilador, pero que aún no tienen sentido), puede, dependiendo de qué tan complejo quiera hacerlo, usar la afirmación estática de Boost o la biblioteca Boost concept_check.

Con un compilador actualizado, tiene un incorporado static_assertque podría usarse en su lugar.

  • Sí, siempre pensé que las plantillas son lo más parecido a escribir en C++. Si tiene todos los elementos necesarios para una plantilla, se puede utilizar en una plantilla.

    usuario3458

    18 de mayo de 2009 a las 0:12

  • @John: Lo siento, no puedo entender eso. que tipo es T, y de dónde se llama este código? Sin algo de contexto, no tengo posibilidad de entender ese fragmento de código. Pero lo que dije es verdad. Si intentas llamar toString() en un tipo que no tiene un toString función miembro, obtendrá un error de compilación.

    – jalf

    11 de diciembre de 2011 a las 21:44

  • @John: la próxima vez, tal vez deberías ser un poco menos feliz votando negativamente a las personas cuando el problema está en tu código

    – jalf

    12 de diciembre de 2011 a las 8:12

  • @jalf, está bien. +1. Esta fue una gran respuesta solo tratando de hacerla lo mejor. Lo siento por leer mal. Pensé que estábamos hablando de usar el tipo como un parámetro para las clases, no para las plantillas de funciones, que supongo que son miembros de las primeras pero deben invocarse para que el compilador marque.

    – Juan

    12 de diciembre de 2011 a las 10:52

Nosotros podemos usar std::is_base_of y std::enable_if:
(static_assert pueden eliminarse, las clases anteriores pueden implementarse de forma personalizada o utilizarse desde aumentar si no podemos hacer referencia type_traits)

#include <type_traits>
#include <list>

class Base {};
class Derived: public Base {};

#if 0   // wrapper
template <class T> class MyClass /* where T:Base */ {
private:
    static_assert(std::is_base_of<Base, T>::value, "T is not derived from Base");
    typename std::enable_if<std::is_base_of<Base, T>::value, T>::type inner;
};
#elif 0 // base class
template <class T> class MyClass: /* where T:Base */
    protected std::enable_if<std::is_base_of<Base, T>::value, T>::type {
private:
    static_assert(std::is_base_of<Base, T>::value, "T is not derived from Base");
};
#elif 1 // list-of
template <class T> class MyClass /* where T:list<Base> */ {
    static_assert(std::is_base_of<Base, typename T::value_type>::value , "T::value_type is not derived from Base");
    typedef typename std::enable_if<std::is_base_of<Base, typename T::value_type>::value, T>::type base; 
    typedef typename std::enable_if<std::is_base_of<Base, typename T::value_type>::value, T>::type::value_type value_type;

};
#endif

int main() {
#if 0   // wrapper or base-class
    MyClass<Derived> derived;
    MyClass<Base> base;
//  error:
    MyClass<int> wrong;
#elif 1 // list-of
    MyClass<std::list<Derived>> derived;
    MyClass<std::list<Base>> base;
//  error:
    MyClass<std::list<int>> wrong;
#endif
//  all of the static_asserts if not commented out
//  or "error: no type named ‘type’ in ‘struct std::enable_if<false, ...>’ pointing to:
//  1. inner
//  2. MyClass
//  3. base + value_type
}

Plantillas de C++ que aceptan solo ciertos tipos
barry carr

Por lo que sé, esto no es posible actualmente en C++. Sin embargo, hay planes para agregar una función llamada “conceptos” en el nuevo estándar C++0x que proporciona la funcionalidad que está buscando. Esta artículo de wikipedia about C++ Concepts lo explicará con más detalle.

Sé que esto no soluciona su problema inmediato, pero hay algunos compiladores de C++ que ya comenzaron a agregar funciones del nuevo estándar, por lo que es posible encontrar un compilador que ya haya implementado la función de conceptos.

  • Desafortunadamente, los conceptos se han eliminado del estándar.

    – macbirdie

    24 de julio de 2009 a las 8:38

  • Deben adoptarse restricciones y conceptos para C++20.

    – Petr Javorik

    28/06/2018 a las 17:50

  • Es posible incluso sin conceptos, utilizando static_assert y SFINAE, como muestran las otras respuestas. El problema restante para alguien que viene de Java o C#, o Haskell(…) es que el compilador de C++20 no verifica la definición con los conceptos requeridos, lo que hacen Java y C#.

    – usuario7610

    15 de diciembre de 2019 a las 19:55


Plantillas de C++ que aceptan solo ciertos tipos
Nueva Hampshire_

Un equivalente que solo acepta tipos T derivados del tipo List parece

template<typename T, 
         typename std::enable_if<std::is_base_of<List, T>::value>::type* = nullptr>
class ObservableList
{
    // ...
};

  • Desafortunadamente, los conceptos se han eliminado del estándar.

    – macbirdie

    24 de julio de 2009 a las 8:38

  • Deben adoptarse restricciones y conceptos para C++20.

    – Petr Javorik

    28/06/2018 a las 17:50

  • Es posible incluso sin conceptos, utilizando static_assert y SFINAE, como muestran las otras respuestas. El problema restante para alguien que viene de Java o C#, o Haskell(…) es que el compilador de C++20 no verifica la definición con los conceptos requeridos, lo que hacen Java y C#.

    – usuario7610

    15 de diciembre de 2019 a las 19:55


Plantillas de C++ que aceptan solo ciertos tipos
Comunidad

Creo que todas las respuestas anteriores han perdido de vista el bosque por los árboles.

genéricos de Java no son lo mismo que las plantillas; ellos usan borrado de tipoel cual es un técnica dinámicaen vez de polimorfismo en tiempo de compilacióncual es técnica estática. Debería ser obvio por qué estas dos tácticas tan diferentes no funcionan bien.

En lugar de intentar usar una construcción de tiempo de compilación para simular una de tiempo de ejecución, veamos qué extends realmente lo hace: según Stack Overflow y Wikipediaextends se usa para indicar subclases.

C++ también admite subclases.

También muestra una clase de contenedor, que utiliza el borrado de tipo en forma de genérico, y se extiende para realizar una verificación de tipo. En C++, debe hacer la maquinaria de borrado de tipos usted mismo, lo cual es simple: haga un puntero a la superclase.

Envolvámoslo en un typedef, para que sea más fácil de usar, en lugar de hacer una clase completa, et voila:

typedef std::list<superclass*> subclasses_of_superclass_only_list;

Por ejemplo:

class Shape { };
class Triangle : public Shape { };

typedef std::list<Shape*> only_shapes_list;
only_shapes_list shapes;

shapes.push_back(new Triangle()); // Works, triangle is kind of shape
shapes.push_back(new int(30)); // Error, int's are not shapes

Ahora, parece que List es una interfaz que representa una especie de colección. Una interfaz en C++ sería simplemente una clase abstracta, es decir, una clase que implementa nada más que métodos virtuales puros. Con este método, puede implementar fácilmente su ejemplo de Java en C++, sin ningún concepto o especialización de plantilla. También funcionaría tan lento como los genéricos de estilo Java debido a las búsquedas de tablas virtuales, pero esto a menudo puede ser una pérdida aceptable.

  • No soy partidario de las respuestas que usan frases como “debería ser obvio” o “todo el mundo sabe”, y luego explican lo que es obvio o universalmente conocido. Obvio es relativo al contexto, la experiencia y el contexto de la experiencia. Tales declaraciones son inherentemente groseras.

    – 3Dave

    08/07/2016 a las 23:31

  • @DavidLively Son dos años demasiado tarde para criticar esta respuesta por etiqueta, pero tampoco estoy de acuerdo contigo en este caso específico; Expliqué por qué las dos técnicas no van juntas. antes de diciendo que era obvio, no después. Proporcioné el contexto y luego dije que la conclusión de ese contexto era obvia. Eso no encaja exactamente en tu molde.

    – Alicia

    10 de julio de 2016 a las 5:30


  • El autor de esta respuesta dijo que algo era obvio después de hacer un trabajo pesado. No creo que el autor pretendiera decir que la solución era obvia.

    – Lucas Gehorsam

    13 de noviembre de 2019 a las 0:53

  • No es del todo obvio por qué las dos técnicas no funcionan bien juntas, o incluso que tengan que hacerlo, ya que las restricciones de parámetros de plantilla dpm tienen que ser las mismas que cualquiera.

    –Robin Davies

    20 de septiembre de 2021 a las 6:47

  • No es del todo obvio por qué las dos técnicas no funcionan bien juntas, o incluso que tienen que hacerlo, ya que las restricciones de parámetros de la plantilla no tienen que ser las mismas. Incluso Strousstrup se sorprendió de que el problema no se abordara en C++0x, dado que estaba en lo más alto de su lista de prioridades. El cuerpo de metaprogramación de plantilla que se ha proporcionado en su lugar es imperdonable. Una forma concisa de especificar “las clases coincidentes deben implementar estos métodos (virtualmente o no virtualmente)” habría abordado el 99 % de los requisitos para los programadores que no son STL. (Veterano de C++ de más de 35 años)

    –Robin Davies

    20 de septiembre de 2021 a las 7:02

¿Ha sido útil esta solución?