¿Por qué no puedo usar el valor flotante como parámetro de plantilla?

120

Cuando intento usarlo floatcomo parámetro de plantilla, el compilador pide este código, mientras que intfunciona bien.

¿Es porque no puedo usarlo floatcomo parámetro de plantilla?

#include<iostream>
using namespace std;

template <class T, T defaultValue>
class GenericClass
{
private:
    T value;
public:
    GenericClass()
    {
        value = defaultValue;
    }

    T returnVal()
    {
        return value;
    }
}; 


int main()
{
    GenericClass <int, 10> gcInteger;
    GenericClass < float, 4.6f> gcFlaot;

    cout << "\n sum of integer is "<<gcInteger.returnVal();
    cout << "\n sum of float is "<<gcFlaot.returnVal();

    return 0;       
}

Error:

main.cpp: In function `int main()':
main.cpp:25: error: `float' is not a valid type for a template constant parameter
main.cpp:25: error: invalid type in declaration before ';' token

main.cpp:28: error: request for member `returnVal' in `gcFlaot',
                    which is of non-class type `int'

Estoy leyendo "Estructuras de datos para programadores de juegos" de Ron Penton, el autor pasa un float, pero cuando lo intento no parece compilarse.

yokks
fuente
1
¿El autor realmente lo usa floatcomo un parámetro de plantilla que no es de tipo ? ¿En qué capítulo es ese?
K-Ballo
1
Lo encontré, está en "Uso de valores como parámetros de plantilla" ...
K-Ballo

Respuestas:

37

El estándar actual de C ++ no permite float(es decir, números reales) o literales de cadenas de caracteres que se utilicen como parámetros de plantilla que no sean de tipo . Por supuesto, puede utilizar los tipos floaty char *como argumentos normales.

¿Quizás el autor está usando un compilador que no sigue el estándar actual?

Filip Roséen - refp
fuente
8
Proporcione un enlace o una copia de la sección relevante de la norma
thecoshman
2
@thecoshman, la sección relevante del estándar + más información está disponible en mi respuesta (recientemente publicada).
Filip Roséen - Refp
1
En C ++ 11, es prácticamente posible utilizar una cadena de caracteres literal como un parámetro de plantilla que no sea de tipo. Si su plantilla toma un paquete de caracteres template<char ...cs>, entonces el literal de cadena se puede convertir en dicho paquete en el momento de la compilación. Aquí hay una demostración de ideone . (La demostración es C ++ 14, pero es fácil trasladarla a C ++ 11, std::integer_sequencees la única dificultad)
Aaron McDaid
Tenga en cuenta que puede utilizarlo char &*como parámetro de plantilla si define el literal en otro lugar. Funciona bastante bien como solución.
StenSoft
137

LA RESPUESTA SIMPLE

El estándar no permite puntos flotantes como argumentos de plantilla que no sean de tipo , sobre los cuales se puede leer en la siguiente sección del estándar C ++ 11;

14.3.2 / 1 Argumentos de no tipo de plantilla [temp.arg.nontype]

Un argumento de plantilla para un parámetro de plantilla que no sea de tipo ni de plantilla será uno de los siguientes:

  • para un parámetro de plantilla que no es de tipo de tipo integral o enumeración, una expresión constante convertida (5.19) del tipo del parámetro de plantilla;

  • el nombre de un parámetro de plantilla que no es de tipo; o

  • una expresión constante (5.19) que designa la dirección de un objeto con duración de almacenamiento estático y enlace externo o interno o una función con enlace externo o interno, incluidas las plantillas de función y los identificadores de plantilla de función, pero excluyendo miembros de clase no estáticos, expresada (ignorando paréntesis) como & id-expression, excepto que & puede omitirse si el nombre se refiere a una función o matriz y se omitirá si el parámetro de plantilla correspondiente es una referencia; o

  • una expresión constante que se evalúa como un valor de puntero nulo (4.10); o

  • una expresión constante que se evalúa como un valor de puntero de miembro nulo (4.11); o

  • un puntero a miembro expresado como se describe en 5.3.1.


Pero ... pero ... ¿¡POR QUÉ !?

Probablemente se deba al hecho de que los cálculos de coma flotante no se pueden representar de manera exacta. Si estuviera permitido, podría / resultaría en un comportamiento erróneo / extraño al hacer algo como esto;

func<1/3.f> (); 
func<2/6.f> ();

Queríamos llamar a la misma función dos veces, pero este podría no ser el caso, ya que no se garantiza que la representación de punto flotante de los dos cálculos sea exactamente la misma.


¿Cómo representaría valores de punto flotante como argumentos de plantilla?

Con C++11podría escribir algunas expresiones constantes bastante avanzadas ( constexpr ) que calcularían el numerador / denominador de un tiempo de compilación de valor flotante y luego pasarían estos dos como argumentos enteros separados.

Recuerde definir algún tipo de umbral para que los valores de punto flotante cercanos entre sí produzcan el mismo numerador / denominador ; de lo contrario, no tiene sentido, ya que producirá el mismo resultado mencionado anteriormente como una razón para no permitir valores de punto flotante como no tipo argumentos de plantilla .

Filip Roséen - refp
fuente
56
La solución de C ++ 11 se <ratio>describe en §20.10 como "Aritmética racional en tiempo de compilación". Lo que va directo a tu ejemplo.
Potatoswatter
1
@Potatoswatter afaik ¿no hay ningún método en el STL para convertir un flotante en numerador / denominador usando <ratio>?
Filip Roséen - refp
3
Esto realmente no da una explicación convincente. El punto del punto flotante es que representa valores exactamente. Puede tratar los números que tiene como aproximaciones a otra cosa y, a menudo, es útil hacerlo, pero los números en sí son exactos.
tmyklebu
4
@ FilipRoséen-refp: Todos los números en coma flotante son exactos. La aritmética de coma flotante está bien definida en todos los objetivos que conozco. La mayoría de las operaciones de coma flotante producen resultados de coma flotante. Puedo apreciar que el comité no quiere forzar a los implementadores del compilador a implementar la aritmética de punto flotante posiblemente extraña del objetivo, pero no creo que "la aritmética es diferente de la aritmética de enteros" sea una buena razón para prohibir los argumentos de plantilla de punto flotante. Es una restricción arbitraria al final del día.
tmyklebu
5
@iheanyi: ¿El estándar dice qué 12345 * 12345es? (Se hace permitir que intlos parámetros de plantilla a pesar de que no especifica el ancho de una int firmado o si dicha expresión es UB.)
tmyklebu
34

Solo para proporcionar una de las razones por las que esto es una limitación (al menos en la norma actual).

Al hacer coincidir especializaciones de plantilla, el compilador coincide con los argumentos de la plantilla, incluidos los argumentos que no son de tipo.

Por su propia naturaleza, los valores de coma flotante no son exactos y su implementación no está especificada por el estándar C ++. Como resultado, es difícil decidir cuándo coinciden realmente dos argumentos que no son de tipo de punto flotante:

template <float f> void foo () ;

void bar () {
    foo< (1.0/3.0) > ();
    foo< (7.0/21.0) > ();
}

Estas expresiones no producen necesariamente el mismo "patrón de bits", por lo que no sería posible garantizar que utilizaran la misma especialización, sin una redacción especial para cubrir esto.

Richard Corden
fuente
16
Este es casi un argumento para prohibir por completo los flotadores del idioma. O, como mínimo, prohibir el ==operador :-) Ya aceptamos esta inexactitud en tiempo de ejecución, ¿por qué no también en tiempo de compilación?
Aaron McDaid
3
De acuerdo con @AaronMcDaid, este no es un gran argumento. Así que debes tener cuidado con la definición. ¿Y qué? Siempre que funcione para las cosas que obtienes de las constantes, ya es una gran mejora.
Einpoklum
1
C ++ 20 ahora permite float (otros tipos de objeto) como parámetros de plantilla que no son de tipo. Aún así, C ++ 20 no especifica la implementación flotante. Esto muestra que einpoklum y Aaron tienen razón.
Andreas H.
20

De hecho, no puede usar literales flotantes como parámetros de plantilla. Consulte la sección 14.1 ("Un parámetro de plantilla que no sea de tipo deberá tener uno de los siguientes tipos (opcionalmente calificado para cv) ...") del estándar.

Puede utilizar una referencia al flotante como parámetro de plantilla:

template <class T, T const &defaultValue>
class GenericClass

.
.

float const c_four_point_six = 4.6; // at global scope

.
.

GenericClass < float, c_four_point_six> gcFlaot;
sombra de Luna
fuente
11
Usted puede. pero no hace lo mismo. No puede usar la referencia como una constante en tiempo de compilación.
12

Envuelva el (los) parámetro (s) en su propia clase como constexprs. Efectivamente, esto es similar a un rasgo, ya que parametriza la clase con un conjunto de flotantes.

class MyParameters{
    public:
        static constexpr float Kd =1.0f;
        static constexpr float Ki =1.0f;
        static constexpr float Kp =1.0f;
};

y luego crea una plantilla tomando el tipo de clase como parámetro

  template <typename NUM, typename TUNING_PARAMS >
  class PidController {

      // define short hand constants for the PID tuning parameters
      static constexpr NUM Kp = TUNING_PARAMS::Kp;
      static constexpr NUM Ki = TUNING_PARAMS::Ki;
      static constexpr NUM Kd = TUNING_PARAMS::Kd;

      .... code to actually do something ...
};

y luego úsalo así ...

int main (){
    PidController<float, MyParameters> controller;
    ...
    ...
}

Esto permite al compilador garantizar que solo se cree una única instancia del código para cada instanciación de plantilla con el mismo paquete de parámetros. Eso soluciona todos los problemas y puede usar flotadores y dobles como constexpr dentro de la clase con plantilla.

Andrew Goedhart
fuente
5

Si está de acuerdo con tener un valor predeterminado fijo por tipo, puede crear un tipo para definirlo como una constante y especializarlo según sea necesario.

template <typename T> struct MyTypeDefault { static const T value; };
template <typename T> const T MyTypeDefault<T>::value = T();
template <> struct MyTypeDefault<double> { static const double value; };
const double MyTypeDefault<double>::value = 1.0;

template <typename T>
class MyType {
  public:
    MyType() { value = MyTypeDefault<T>::value; }
  private:
    T value;
 };

Si tiene C ++ 11, puede usar constexpr al definir el valor predeterminado. Con C ++ 14, MyTypeDefault puede ser una variable de plantilla que es un poco más limpia sintácticamente.

//C++14
template <typename T> constexpr T MyTypeDefault = T();
template <> constexpr double MyTypeDefault<double> = 1.0;

template <typename T>
class MyType {
  private:
    T value = MyTypeDefault<T>;
 };
Mateo Fioravante
fuente
2

Las otras respuestas dan buenas razones por las que probablemente no desee parámetros de plantilla de punto flotante, pero lo más importante en mi opinión es que la igualdad usando '==' y la igualdad bit a bit no son lo mismo:

  1. -0.0 == 0.0, pero 0.0y -0.0no son iguales a nivel de bits

  2. NAN != NAN

Ninguno de los dos tipos de igualdad es una buena alternativa para la igualdad de tipos: por supuesto, el punto 2. ==invalida el uso para determinar la igualdad de tipos. En su lugar, se podría usar la igualdad bit a bit, pero eso x != yno implica eso MyClass<x>y MyClass<y>son tipos diferentes (por 2.), lo que sería bastante extraño.

Matthieu
fuente
1

Siempre puedes fingirlo ...

#include <iostream>

template <int NUM, int DEN>
struct Float
{
    static constexpr float value() { return (float)NUM / (float)DEN; }
    static constexpr float VALUE = value();
};

template <class GRAD, class CONST>
struct LinearFunc
{
    static float func(float x) { return GRAD::VALUE*x + CONST::VALUE; }
};


int main()
{
    // Y = 0.333 x + 0.2
    // x=2, y=0.866
    std::cout << " func(2) = "
              << LinearFunc<Float<1,3>, Float<1,5> > ::func(2) << std::endl;
}

Ref: http://code-slim-jim.blogspot.jp/2013/06/c11-no-floats-in-templates-wtf.html

Ashley Smart
fuente
3
A float! = Número racional. Las dos son ideas muy separadas. Uno se calcula mediante una mantisa y un exponente, el otro es, bueno, un racional; no todos los valores representables por un racional son representables por a float.
Richard J. Ross III
2
@ RichardJ.RossIII A floates definitivamente un número racional, pero hay floats que no se pueden representar como razones de dos ints. La mantisa es un número entero y el exponente 2 ^ es un número entero
Caleth
1

Si no necesita que el doble sea una constante en tiempo de compilación, puede pasarlo como un puntero:

#include <iostream>

extern const double kMyDouble = 0.1;;

template <const double* MyDouble>
void writeDouble() {
   std::cout << *MyDouble << std::endl; 
}

int main()
{
    writeDouble<&kMyDouble>();
   return 0;
}
user3233025
fuente
Una referencia es probablemente mejor, vea la respuesta de
@moonshadow
1
¿Esto realmente se reduce correctamente en tiempo de compilación?
Ant6n
1

Comenzando con C ++ 20 esto es posible .

Esto también da la respuesta a la pregunta original:

Why can't I use float value as a template parameter?

Porque nadie lo implementó en el estándar todavía. No hay ninguna razón fundamental.

En C ++ 20, los parámetros de plantilla que no son de tipo ahora pueden ser flotantes e incluso objetos de clase.

Hay algunos requisitos sobre los objetos de clase (deben ser un tipo literal ) y cumplen algunos otros requisitos para excluir los casos patológicos, como el operador definido por el usuario == ( Detalles ).

Incluso podemos usar auto

template <auto Val>
struct Test {
};

struct A {};
static A aval;
Test<aval>  ta;
Test<A{}>  ta2;
Test<1.234>  tf;
Test<1U>  ti;

Tenga en cuenta que GCC 9 (y 10) implementa parámetros de plantilla de clase que no son de tipo, pero todavía no para flotantes .

Andreas H.
fuente
0

Si solo desea representar una precisión fija, puede usar una técnica como esta para convertir un parámetro flotante en un int.

Por ejemplo, se podría crear una matriz con un factor de crecimiento de 1,75 de la siguiente manera, asumiendo 2 dígitos de precisión (dividir por 100).

template <typename _Kind_, int _Factor_=175>
class Array
{
public:
    static const float Factor;
    _Kind_ * Data;
    int Size;

    // ...

    void Resize()
    {
         _Kind_ * data = new _Kind_[(Size*Factor)+1];

         // ...
    }
}

template<typename _Kind_, int _Factor_>
const float Array<_kind_,_Factor_>::Factor = _Factor_/100;

Si no le gusta la representación de 1,75 como 175 en la lista de argumentos de la plantilla, siempre puede envolverlo en alguna macro.

#define FloatToIntPrecision(f,p) (f*(10^p))

template <typename _Kind_, int _Factor_=FloatToIntPrecision(1.75,2)>
// ...
jurujen
fuente
debería ser de lo ...::Factor = _Factor_/100.0;contrario será una división entera.
alfC