Plantillas C ++ que aceptan solo ciertos tipos

159

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

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

Esto se hace usando la palabra clave "extend".

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

mgamer
fuente
pregunta bastante antigua ... Creo que lo que falta aquí (también en las respuestas) es que los genéricos de Java no son realmente un equivalente de plantillas en C ++. Hay similitudes, pero en mi opinión, uno 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;)
idclev 463035818

Respuestas:

104

Sugiero usar la función de aserción estática de Boost en concierto con is_base_ofla biblioteca Boost Type Traits:

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 otros casos más simples, puede simplemente declarar hacia adelante una plantilla global, pero solo definirla (especializarse 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.

[EDIT menor 12/12/2013: el uso de una plantilla declarada pero no definida dará como resultado mensajes de error del vinculador , no del compilador.]

j_random_hacker
fuente
Las afirmaciones estáticas también son agradables. :)
macbirdie
55
@John: Me temo que la especialización solo coincidirá myBaseTypeexactamente. Antes de descartar Boost, debe saber que la mayor parte es un código de plantilla de solo encabezado, por lo que no hay costos de memoria o tiempo en tiempo de ejecución para las cosas que no usa. Además, las cosas particulares que usaría aquí ( BOOST_STATIC_ASSERT()y is_base_of<>) se pueden implementar usando solo declaraciones (es decir, sin definiciones reales de funciones o variables) para que tampoco tomen espacio ni tiempo.
j_random_hacker
50
C ++ 11 ha llegado. Ahora podemos usar static_assert(std::is_base_of<List, T>::value, "T must extend list").
Siyuan Ren
2
Por cierto, la razón por la que el paréntesis doble es necesario es que 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 macro.
jfritz42
1
@Andreyua: Realmente no entiendo lo que falta. Puede intentar declarar una variable my_template<int> x;o my_template<float**> y;verificar que el compilador lo permita, y luego declarar una variable my_template<char> z;y verificar que no.
j_random_hacker
134

Esto generalmente no está justificado en C ++, como han señalado otras respuestas aquí. En C ++ tendemos a definir tipos genéricos basados ​​en otras restricciones que no sean "hereda de esta clase". Si realmente quería hacer eso, es bastante 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 las personas esperan en C ++. Es mejor usar trucos como definir tus propios rasgos. Por ejemplo, tal vez observable_listquiera aceptar cualquier tipo de envase que tiene las typedefs const_iteratory una beginy endfunción miembro que los retornos const_iterator. Si restringe esto a clases que heredan de listentonces, un usuario que tiene su propio tipo que no hereda listpero que proporciona estas funciones miembro y typedefs no podrá usar su observable_list.

Hay dos soluciones a este problema, una de ellas es no restringir nada y confiar en la tipificación de patos. 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. El gran inconveniente de esta solución es que implica una escritura adicional que puede considerarse 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...
};

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

Rapptz
fuente
2
Nunca me di cuenta de que las plantillas C ++ usan la escritura de pato hasta hoy. Tipo de extraño!
Andy
2
Dadas las amplias restricciones de política introducidas por C ++ C , no estoy seguro de por qué template<class T:list>es un concepto tan ofensivo. Gracias por el consejo.
bvj
61

La solución simple, que nadie ha mencionado todavía, es ignorar el problema. Si intento usar un inttipo 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 eso falla, genera un error de compilación.

El único problema con eso es que los mensajes de error que recibas 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 verificar que los tipos utilizados sean 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 lo complejo que desee hacerlo, usar la afirmación estática de Boost o la biblioteca Boost concept_check.

Con un compilador actualizado, tiene un built_in static_assert, que podría usarse en su lugar.

jalf
fuente
77
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 usar en una plantilla.
@ John: Lo siento, no puedo entenderlo. ¿Qué tipo es Ty desde dónde se llama este código? Sin algún contexto, no tengo ninguna posibilidad de entender ese fragmento de código. Pero lo que dije es cierto. Si intenta llamar toString()a un tipo que no tiene una toStringfunción miembro, obtendrá un error de compilación.
jalf
@John: la próxima vez, tal vez deberías ser un poco menos desencadenante cuando el problema está en tu código
jalf
@jalf, ok. +1. Esta fue una gran respuesta solo tratando de hacerlo lo mejor posible. Perdón 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 es necesario invocar para que el compilador marque.
John
13

Podemos usar std::is_base_ofy std::enable_if:
( static_assertse puede eliminar, las clases anteriores se pueden implementar de forma personalizada o se pueden usar desde boost 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
}
Firda
fuente
13

Hasta donde sé, esto no es posible actualmente en C ++. Sin embargo, hay planes para agregar una característica llamada "conceptos" en el nuevo estándar C ++ 0x que brinde la funcionalidad que está buscando. Este artículo de Wikipedia sobre C ++ Concepts lo explicará con más detalle.

Sé que esto no soluciona su problema inmediato, pero hay algunos compiladores de C ++ que ya han comenzado a agregar características del nuevo estándar, por lo que podría ser posible encontrar un compilador que ya haya implementado la función de conceptos.

Barry Carr
fuente
44
Desafortunadamente, los conceptos se han eliminado del estándar.
macbirdie
44
Se deben adoptar restricciones y conceptos para C ++ 20.
Petr Javorik
Es posible incluso sin conceptos, usando static_asserty 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 hace una verificación de definición contra los conceptos requeridos, lo que hacen Java y C #.
user7610
10

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

Los genéricos de Java no son lo mismo que las plantillas ; usan el tipo de borrado , que es una técnica dinámica , en lugar de compilar el polimorfismo de tiempo , que es una técnica estática . Debería ser obvio por qué estas dos tácticas muy diferentes no funcionan bien.

En lugar de intentar usar una construcción en tiempo de compilación para simular una en tiempo de ejecución, veamos lo que extendsrealmente hace: de acuerdo con Stack Overflow y Wikipedia , se usa extender 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 tipo usted mismo, que es simple: haga un puntero a la superclase.

Vamos a envolverlo en un typedef, para que sea más fácil de usar, en lugar de hacer una clase completa, y listo:

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, podría 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.

Alicia
fuente
3
No soy fanático 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
2
@DavidLively Es aproximadamente dos años demasiado tarde para criticar esta respuesta por etiqueta, pero tampoco estoy de acuerdo con usted en este caso específico; Le expliqué por qué las dos técnicas no van juntas antes de decir que era obvio, no después. Proporcioné el contexto, y luego dije que la conclusión de ese contexto era obvia. Eso no se ajusta exactamente a tu molde.
Alice
El autor de esta respuesta dijo que algo era obvio después de hacer un trabajo pesado. No creo que el autor tuviera la intención de decir que la solución era obvia.
Luke Gehorsam el
10

Un equivalente que solo acepta tipos T derivados del tipo Lista se parece a

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

Resumen ejecutivo: no hagas eso.

La respuesta de j_random_hacker te dice cómo hacer esto. Sin embargo, también me gustaría señalar que usted debe no hacer esto. El objetivo de las plantillas es que pueden aceptar cualquier tipo compatible, y las restricciones de tipo de estilo Java lo rompen.

Las restricciones de tipo de Java son un error, no una característica. Están allí porque Java borra los tipos genéricos, por lo que Java no puede descubrir cómo llamar a los métodos basándose solo en el valor de los parámetros de tipo.

C ++ por otro lado no tiene tal restricción. Los tipos de parámetros de plantilla pueden ser de cualquier tipo compatible con las operaciones con las que se utilizan. No tiene que haber una clase base común. Esto es similar al "Duck Typing" de Python, pero hecho en tiempo de compilación.

Un ejemplo simple que muestra el poder de las plantillas:

// Sum a vector of some type.
// Example:
// int total = sum({1,2,3,4,5});
template <typename T>
T sum(const vector<T>& vec) {
    T total = T();
    for (const T& x : vec) {
        total += x;
    }
    return total;
}

Esta función de suma puede sumar un vector de cualquier tipo que admita las operaciones correctas. Funciona con primitivas como int / long / float / double y tipos numéricos definidos por el usuario que sobrecargan el operador + =. Diablos, incluso puedes usar esta función para unir cadenas, ya que admiten + =.

No es necesario el encajonamiento / desempaquetado de primitivas.

Tenga en cuenta que también construye nuevas instancias de T usando T (). Esto es trivial en C ++ usando interfaces implícitas, pero no es realmente posible en Java con restricciones de tipo.

Si bien las plantillas de C ++ no tienen restricciones de tipo explícitas, siguen siendo de tipo seguro y no se compilarán con código que no sea compatible con las operaciones correctas.

catphive
fuente
2
Si está sugiriendo plantillas nunca especializadas, ¿puede explicar también por qué está en el idioma?
1
Entiendo su punto, pero si su argumento de plantilla debe derivarse de un tipo específico, entonces es mejor tener un mensaje fácil de interpretar de static_assert que el vómito de error normal del compilador.
jhoffman0x
1
Sí, C ++ es más expresivo aquí, pero aunque eso es generalmente algo bueno (porque podemos expresar más con menos), a veces queremos limitar deliberadamente el poder que nos damos, para tener la certeza de que entendemos completamente un sistema.
j_random_hacker
La especialización de tipo @Curg es útil cuando desea aprovechar algo que solo se puede hacer para ciertos tipos. por ejemplo, un booleano es ~ normalmente ~ un byte cada uno, aunque un byte puede ~ normalmente ~ contener 8 bits / booleanos; una clase de colección de plantillas puede (y en el caso de std :: map does) especializarse para boolean para que pueda empaquetar los datos más estrechamente para conservar la memoria.
thecoshman 01 de
Además, para aclarar, esta respuesta no es "nunca especialice plantillas", es decir, no use esa función para tratar de limitar qué tipos se pueden usar con una plantilla.
thecoshman 01 de
6

Eso no es posible en C ++ simple, pero puede verificar los parámetros de la plantilla en tiempo de compilación a través de la Comprobación de conceptos, por ejemplo, utilizando BCCL de Boost .

A partir de C ++ 20, los conceptos se están convirtiendo en una característica oficial del lenguaje.

Macbirdie
fuente
2
Bueno, es posible, pero la verificación de conceptos sigue siendo una buena idea. :)
j_random_hacker
En realidad quise decir que no era posible en C ++ "simple". ;)
macbirdie
5
class Base
{
    struct FooSecurity{};
};

template<class Type>
class Foo
{
    typename Type::FooSecurity If_You_Are_Reading_This_You_Tried_To_Create_An_Instance_Of_Foo_For_An_Invalid_Type;
};

Asegúrese de que las clases derivadas hereden la estructura de FooSecurity y el compilador se alterará en todos los lugares correctos.

Stuart
fuente
@Zehelvion Type::FooSecurityse usa en la clase de plantilla. Si la clase, aprobada en el argumento de plantilla, no lo ha hecho FooSecurity, intentar usarla causa un error. Es seguro que si la clase pasada en el argumento de plantilla no tiene FooSecurity, no se deriva de ella Base.
GingerPlusPlus
2

Uso de concepto de C ++ 20

https://en.cppreference.com/w/cpp/language/constraints cppreference está dando el caso de uso de herencia como un ejemplo de concepto explícito:

template <class T, class U>
concept Derived = std::is_base_of<U, T>::value;
 
template<Derived<Base> T>
void f(T);  // T is constrained by Derived<T, Base>

Para múltiples bases, supongo que la sintaxis será:

template <class T, class U, class V>
concept Derived = std::is_base_of<U, T>::value || std::is_base_of<V, T>::value;
 
template<Derived<Base1, Base2> T>
void f(T);

Parece que GCC 10 lo implementó: https://gcc.gnu.org/gcc-10/changes.html y puede obtenerlo como PPA en Ubuntu 20.04 . https://godbolt.org/ Mi GCC 10.1 local aún no lo reconocía concept, así que no estoy seguro de lo que está sucediendo.

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
fuente
1

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

No.

Dependiendo de lo que intente lograr, puede haber sustitutos adecuados (o incluso mejores).

He revisado algunos códigos STL (en Linux, creo que es el que deriva de la implementación de SGI). Tiene "aserciones conceptuales"; por ejemplo, si necesita un tipo que comprenda *xy ++x, la aserción del concepto contendría ese código en una función de no hacer nada (o algo similar). Requiere cierta sobrecarga, por lo que puede ser inteligente colocarlo en una macro cuya definición depende #ifdef debug.

Si la relación de subclase es realmente lo que desea saber, podría afirmar en el constructor que T instanceof list(excepto que está "deletreada" de manera diferente en C ++). De esa manera, puede probar su salida del compilador sin poder verificarlo por usted.

Jonas Kölker
fuente
1

No hay una palabra clave para tales comprobaciones de tipo, pero puede poner un código que al menos fallará de manera ordenada:

(1) Si desea que una plantilla de función solo acepte parámetros de una determinada clase base X, asígnela a una referencia X en su función. (2) Si desea aceptar funciones pero no primitivas o viceversa, o desea filtrar clases de otras maneras, llame a una función auxiliar de plantilla (vacía) dentro de su función que solo está definida para las clases que desea aceptar.

Puede usar (1) y (2) también en las funciones miembro de una clase para forzar estos controles de tipo en toda la clase.

Probablemente pueda ponerlo en una Macro inteligente para aliviar su dolor. :)

Jaap
fuente
-2

Bueno, podrías crear tu plantilla leyendo algo como esto:

template<typename T>
class ObservableList {
  std::list<T> contained_data;
};

Sin embargo, esto hará que la restricción sea implícita, además de que no puede proporcionar nada que parezca una lista. Hay otras formas de restringir los tipos de contenedor utilizados, por ejemplo, haciendo uso de tipos de iteradores específicos que no existen en todos los contenedores, pero nuevamente esto es más una restricción implícita que explícita.

Que yo sepa, una construcción que refleje la declaración de la declaración de Java en toda su extensión no existe en el estándar actual.

Hay formas de restringir los tipos que puede usar dentro de una plantilla que escriba usando typedefs específicos dentro de su plantilla. Esto asegurará que la compilación de la especialización de plantilla para un tipo que no incluye ese tipo de definición en particular fallará, por lo que puede admitir selectivamente / no admitir ciertos tipos.

En C ++ 11, la introducción de conceptos debería facilitarlo, pero tampoco creo que haga exactamente lo que desea.

Timo Geusch
fuente