Por que não posso usar o valor flutuante como um parâmetro do modelo?

120

Quando tento usar floatcomo parâmetro de modelo, o compilador chora por esse código, embora intfuncione bem.

É porque não posso usar floatcomo parâmetro de modelo?

#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;       
}

Erro:

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'

Estou lendo "Data Structures for Game Programmers", de Ron Penton, o autor passa um float, mas quando tento parece que não compila.

yokks
fonte
1
O autor realmente usa floatcomo um parâmetro de modelo sem tipo ? Em que capítulo é isso?
K-ballo
1
Encontrei-o em "Usando valores como parâmetros de modelo" ...
K-ballo

Respostas:

37

O padrão C ++ atual não permite float(ou seja, número real) ou literais de string de caracteres a serem usados ​​como parâmetros de modelo sem tipo . É claro que você pode usar os tipos floate char *como argumentos normais.

Talvez o autor esteja usando um compilador que não segue o padrão atual?

Filip Roséen - refp
fonte
8
Forneça um link ou uma cópia da seção relevante do padrão
thecoshman
2
@thecoshman a seção relevante do padrão + mais informações estão disponíveis em minha resposta (recém-postada).
Filip Roséen - refp
1
No C ++ 11, é quase possível usar um literal de cadeia de caracteres como um parâmetro de modelo sem tipo. Se o seu modelo aceita um pacote de caracteres template<char ...cs>, o literal de string pode ser convertido em tal pacote em tempo de compilação. Aqui está uma demonstração do ideone . (Demo é C ++ 14, mas é fácil transportá-lo de volta para C ++ 11 - std::integer_sequenceé a única dificuldade)
Aaron McDaid
Observe que você pode usar char &*como um parâmetro de modelo se definir o literal em outro lugar. Funciona muito bem como uma solução alternativa.
StenSoft
137

A RESPOSTA SIMPLES

O padrão não permite pontos flutuantes como argumentos de modelo não-tipo , que podem ser lidos na seção seguinte do padrão C ++ 11;

14.3.2 / 1 Argumentos sem tipo de modelo [temp.arg.nontype]

Um argumento-modelo para um parâmetro-modelo sem tipo e sem modelo deve ser um dos seguintes:

  • para um parâmetro-modelo não-tipo do tipo integral ou enumeração, uma expressão constante convertida (5.19) do tipo do parâmetro-modelo;

  • o nome de um parâmetro de modelo sem tipo; ou

  • uma expressão constante (5.19) que designa o endereço de um objeto com duração de armazenamento estático e ligação externa ou interna ou uma função com ligação externa ou interna, incluindo modelos de função e IDs de modelo de função, mas excluindo membros de classe não estáticos, expressa (ignorando parênteses) como & id-expression, exceto que o & pode ser omitido se o nome se referir a uma função ou array e deverá ser omitido se o parâmetro-modelo correspondente for uma referência; ou

  • uma expressão constante que é avaliada como um valor de ponteiro nulo (4.10); ou

  • uma expressão constante que é avaliada como um valor de ponteiro de membro nulo (4.11); ou

  • um indicador para membro expresso conforme descrito em 5.3.1.


Mas .. mas .. POR QUE !?

Provavelmente é devido ao fato de que os cálculos de ponto flutuante não podem ser representados de maneira exata. Se fosse permitido, poderia / resultaria em um comportamento errôneo / estranho ao fazer algo como isso;

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

Pretendíamos chamar a mesma função duas vezes, mas pode não ser o caso, pois a representação de ponto flutuante dos dois cálculos não é garantida como exatamente a mesma.


Como eu representaria valores de ponto flutuante como argumentos de modelo?

Com C++11você poderia escrever algumas expressões constantes bastante avançadas ( constexpr ) que calculariam o numerador / denominador de um tempo de compilação de valor flutuante e então passaria esses dois como argumentos inteiros separados.

Lembre-se de definir algum tipo de limite para que os valores de ponto flutuante próximos uns dos outros produzam o mesmo numerador / denominador , caso contrário, é um tanto inútil, pois produzirá o mesmo resultado mencionado anteriormente como um motivo para não permitir valores de ponto flutuante como não-tipo argumentos do modelo .

Filip Roséen - refp
fonte
56
A solução C ++ 11 é <ratio>, descrita por §20.10 como "aritmética racional em tempo de compilação". O que vai direto ao seu exemplo.
Potatoswatter
1
@Potatoswatter afaik não existe nenhum método no STL para converter um float em numerador / denominador usando <ratio>?
Filip Roséen - refp
3
Isso realmente não dá uma explicação convincente. O ponto flutuante é que ele representa valores exatamente. Você é livre para tratar os números que possui como aproximações de outra coisa, e geralmente é útil fazer isso, mas os números em si são exatos.
tmyklebu
4
@ FilipRoséen-refp: Todos os números de ponto flutuante são exatos. A aritmética de ponto flutuante é bem definida em todos os alvos que conheço. A maioria das operações de ponto flutuante produzem resultados de ponto flutuante. Posso apreciar o comitê não querer forçar os implementadores do compilador a implementar a aritmética de ponto flutuante possivelmente bizarra do destino, mas não acredito que "a aritmética é diferente da aritmética de inteiros" seja um bom motivo para proibir argumentos de modelo de ponto flutuante. É uma restrição arbitrária no final do dia.
tmyklebu
5
@iheanyi: O padrão diz o que 12345 * 12345é? (Ele faz permitir intparâmetros do modelo, mesmo que não especifica a largura de um int assinado ou se essa expressão é UB.)
tmyklebu
34

Apenas para fornecer um dos motivos pelos quais isso é uma limitação (pelo menos no padrão atual).

Ao combinar as especializações do template, o compilador combina os argumentos do template, incluindo argumentos não-tipo.

Por sua própria natureza, os valores de ponto flutuante não são exatos e sua implementação não é especificada pelo padrão C ++. Como resultado, é difícil decidir quando dois argumentos sem tipo de ponto flutuante realmente correspondem:

template <float f> void foo () ;

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

Essas expressões não produzem necessariamente o mesmo "padrão de bits" e, portanto, não seria possível garantir que usassem a mesma especialização - sem um texto especial para cobrir isso.

Richard Corden
fonte
16
Este é quase um argumento para banir inteiramente os flutuadores da linguagem. Ou, no mínimo, banir o ==operador :-) Nós já aceitamos esta imprecisão em tempo de execução, por que não em tempo de compilação também?
Aaron McDaid
3
Concordo com @AaronMcDaid, isso não é um grande argumento. Portanto, você precisa ter cuidado na definição. E daí? Contanto que funcione para coisas que você obtém de constantes, já é uma grande melhoria.
einpoklum
1
C ++ 20 agora permite float (um outro tipo de objeto) como parâmetros de template não-tipo. Ainda assim, o C ++ 20 não especifica a implementação float. Isso mostra que einpoklum e Aaron têm razão.
Andreas H.
20

Na verdade, você não pode usar literais float como parâmetros de modelo. Consulte a seção 14.1 ("Um parâmetro de modelo não-tipo deve ter um dos seguintes tipos (opcionalmente qualificado pelo cv) ...") do padrão.

Você pode usar uma referência ao float como um parâmetro de modelo:

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 da Lua
fonte
11
Você pode. mas não faz a mesma coisa. Você não pode usar a referência como uma constante de tempo de compilação.
12

Envolva o (s) parâmetro (s) em sua própria classe como constexprs. Efetivamente, isso é semelhante a uma característica, pois parametriza a classe com um conjunto de flutuadores.

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

e, em seguida, crie um modelo tomando o tipo de classe 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 ...
};

e então usá-lo assim ...

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

Isso permite que o compilador garanta que apenas uma única instância do código seja criada para cada instanciação do modelo com o mesmo pacote de parâmetros. Isso contorna todos os problemas e você pode usar floats e doubles como constexpr dentro da classe modelada.

Andrew Goedhart
fonte
5

Se você concordar em ter um padrão fixo por tipo, poderá criar um tipo para defini-lo como uma constante e especializá-lo conforme necessário.

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;
 };

Se você tiver o C ++ 11, poderá usar constexpr ao definir o valor padrão. Com C ++ 14, MyTypeDefault pode ser uma variável de modelo que é um pouco mais limpa sintaticamente.

//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>;
 };
Matthew Fioravante
fonte
2

As outras respostas fornecem boas razões pelas quais você provavelmente não quer parâmetros de modelo de ponto flutuante, mas o verdadeiro travão IMO é que a igualdade usando '==' e igualdade bit a bit não são a mesma:

  1. -0.0 == 0.0, mas 0.0e -0.0não são iguais aos bits

  2. NAN != NAN

Nenhum tipo de igualdade é um bom cancelamento para igualdade de tipo: claro, o ponto 2. torna o uso ==inválido para determinar a igualdade de tipo. Pode-se usar a igualdade bit a bit em vez disso, mas x != ynão implica isso MyClass<x>e MyClass<y>são tipos diferentes (por 2.), o que seria um tanto estranho.

Matthieu
fonte
1

Você sempre pode fingir ...

#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
fonte
3
A float! = Número racional. Os dois são ideias muito distintas. Um é calculado por meio de uma mantissa e um expoente, o outro é, bem, um racional - nem todo valor representável por um racional é representável por a float.
Richard J. Ross III
2
@RichardJ.RossIII A floaté definitivamente um número racional, mas há floats que não são representáveis ​​como proporções de dois ints. A mantissa é um inteiro e o expoente 2 ^ é um inteiro
Caleth
1

Se você não precisa que o duplo seja uma constante de tempo de compilação, pode passá-lo como um ponteiro:

#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
fonte
Uma referência é provavelmente melhor, veja a resposta de
@moonshadow
1
Isso realmente reduz adequadamente em tempo de compilação?
Ant6n
1

Começando com C ++ 20, isso é possível .

Isso também dá a resposta à pergunta original:

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

Porque ninguém o implementou no padrão ainda. Não existe uma razão fundamental.

Em C ++ 20, os parâmetros de template não-tipo agora podem ser flutuantes e até mesmo objetos de classe.

Existem alguns requisitos em objetos de classe (eles devem ser um tipo literal ) e cumprir alguns outros requisitos para excluir os casos patológicos, como operador definido pelo usuário == ( Detalhes ).

Podemos até 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;

Observe que o GCC 9 (e 10) implementa parâmetros de template de classe sem tipo, mas não para floats ainda .

Andreas H.
fonte
0

Se você deseja representar apenas uma precisão fixa, pode usar uma técnica como esta para converter um parâmetro float em um int.

Por exemplo, uma matriz com um fator de crescimento de 1,75 poderia ser criada da seguinte maneira, assumindo 2 dígitos de precisão (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;

Se você não gosta da representação de 1,75 como 175 na lista de argumentos do modelo, você pode sempre envolvê-lo em alguma macro.

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

template <typename _Kind_, int _Factor_=FloatToIntPrecision(1.75,2)>
// ...
Jurujen
fonte
deveria ser, ...::Factor = _Factor_/100.0;caso contrário, será uma divisão inteira.
alfC