What's wrong with my class's function to decrease the value?

1

I'm developing a class so I can better manage my items in a more organized way. When the value of the variable is less than the minimum value was to go to the maximum value, however, in practice this is not happening, it will stop some strange values:

By pressing the F2 key 7 times I have this in the console of my algorithm:

Item texto: Testando um float
Valor maximo do item: 0.5
Valor atual: 0.4

Item texto: Testando um float
Valor maximo do item: 0.5
Valor atual: 0.3

Item texto: Testando um float
Valor maximo do item: 0.5
Valor atual: 0.2

Item texto: Testando um float
Valor maximo do item: 0.5
Valor atual: 0.1

Item texto: Testando um float
Valor maximo do item: 0.5
Valor atual: 1.49012e-08

Item texto: Testando um float
Valor maximo do item: 0.5
Valor atual: -0.1

Item texto: Testando um float
Valor maximo do item: 0.5
Valor atual: 0.5

From the value 0.1 it was already supposed to go to the value 0.5 instead of going to the value 1.49012e-08

My class:

class cVarFloat
{
private:
       float            fValue;
       float            fValueSet;
       float            fMax;
       float            fMin;
       char             ItemText[100];
public:
       float            GetMax()
       {
           return this->fMax;
       }

       float            GetValue()
       {
           return this->fValue;
       }

       char*            GetItemText(void)
       {
           return this->ItemText;
       }

       void         SetValue(float fValue)
       {
           this->fValue = fValue;
       }

       /*
           É nessa função que eu estou tendo o problema
       */
       void         DecValue(void)
       {
           if (this->fValue <= this->fMin)
               this->fValue = this->fMax;
           else
               this->fValue -= this->fValueSet;
       }

       void         IncValue(void)
       {
           if (this->fValue >= this->fMax)
               this->fValue = fMin;
           else
               this->fValue += fValueSet;
       }

       cVarFloat(const char* ItemText, float fMin, float fMax, float fValueSet, float fInitValue)
       {
           this->fMin = fMin;
           this->fMax = fMax;
           this->fValueSet = fValueSet;
           this->fValue = fInitValue;
           strcpy(this->ItemText, ItemText);
       }
   };

In my main:

int main()
{
    // Primeiro argumento = Texto da classe
    // Segundo argumento = Valor mínimo
    // Terceiro argumento = Valor máximo
    // Quarto argumento = Valor na qual vai aumentar/diminuir o valor da variável
    // Quinto argumento = Valor inicial da variável

    cVarFloat Teste("Testando um float", 0.0f, 0.5f, 0.1f, 0.5f);

    while (1)
    {
        if (GetAsyncKeyState(VK_F1) & 1)
        {
            Teste.IncValue();
            cout << "Item texto: " << Teste.GetItemText() << "\nValor maximo do item: " << Teste.GetMax() << "\nValor atual: " << Teste.GetValue() << endl << endl;
        }

        if (GetAsyncKeyState(VK_F2) & 1)
        {
            Teste.DecValue();   // problema nessa função
            cout << "Item texto: " << Teste.GetItemText() << "\nValor maximo do item: " << Teste.GetMax() << "\nValor atual: " << Teste.GetValue() << endl << endl;
        }
    }
    return 0;
}
    
asked by anonymous 10.10.2017 / 00:49

4 answers

4

Problem

The problem has to do with comparing equality in floating point values. This becomes a problem because the representation for several of the values is not exact which ends up giving unexpected results!

Consider the following example:

double a = 0.1 + 0.2;
double b = 0.3; 

cout<<(a == b)<<endl; //0 -> falso

Which displays 0 on the console, thus indicating that 0.1+0.2 differs from 0.3 !

See for yourself at Ideone

Actually 0.1 is not possible to accurately represent a double , due to the way it is encoded in the binary system.

Comparing systems:

  • 1/10 is not encoded accurately and finitely in binary
  • 10/3 is not coded accurately and finitely in decimal basis.

This causes 0.1 in binary to be an infinite periodic decimal written as:

0.00011001100110011001100110011001100110011....

Usually noted as:

Sowhenwewrite:

doublec=0.1;

Thevaluethatcreallyhasis

0.1000000000000000055511151231257827021181583404541015625

Oreveninfloat:

floatd=0.1f;//0.100000001490116119384765625

Thatwaspreciselythe1.49012e-08thatappearedontheconsole.Thesevaluesendupbeingshownintheconsoleas0.1eventhoughtheyarenot,andtheymakeitimpossibletocompareexactly.

Thisisjustoneofmanyexamplesofvaluesthatarenotexactlyrepresentableinbinary.

Solution

Inyoursimplecaseyoucoulduseacomparisonwithmarginoferrortoreplacetheequality,usuallycalledepsilon:

floatepsilon=0.0001f;if(fabs(a-b)<epsilon){

Herewecomparewhetherthedifferenceofthetwoislowenoughthatweconsiderthemequal.Notethatthefabsfunctionofthe<cmath>librarywasusedtogettheabsolutevalueofafloat.

Howyouwanttocomparewithlessthanorequalyoucanevencreateafunctionthatdoesthisautomatically:

booligualMenor(floata,floatb,floatepsilon){if(fabs(a-b)<epsilon)returntrue;returna-b<0;}

Nowyouonlyhavetoapplytoyourifs:

voidDecValue(void){//chamandoafunçãoquecomparapassandoumepsilonrazoávelparaocasoemquestãoif(igualMenor(this->fValue,this->fMin,0.0001f))this->fValue=this->fMax;elsethis->fValue-=this->fValueSet;}

Thiswillnotpreventcoutfromdisplayingthe0.1valueinitsexactrepresentation,somethingwecanforceusingcout<<fixedthusensuringthatthevalueiswhatweexpectedvisually.

View your code with this ideone solution applied
I slightly changed the data entry so that the result would be presentable in the Ideone

Improvements

This solution although it works may also bring some problems with regard to epsilon, since it is not scaled to a and b . In this case you can choose to divide the result of the subtraction by one of the values, which reduces the scale of the same as epsilon:

fabs((a-b)/b) < epsilon

However, this will cause problems with divisions with 0 . So I would have to extend the logic to something much more complex (which would be exaggerated for your example):

bool quaseIgual(float a, float b, float epsilon) {
    float absA = fabs(a);
    float absB = fabs(b);
    float diff = fabs(a - b);

    if (a == b) { // atalho para resolver infinitos
        return true;
    } else if (a == 0 || b == 0 || diff < FLT_MIN) {
        // a ou b são zeros e extremamente perto entre si
        // erro relativo é ignorável neste caso
        return diff < (epsilon * FLT_MIN );
    } else { // usar erro relativo
        return diff / min((absA + absB), FLT_MAX ) < epsilon;
    }
}

Including <float.h> to access constants FLT_MIN and FLT_MAX

References:

10.10.2017 / 03:37
0

The problem is that floating-point arithmetic (float) does not have infinite precision. If you add floats and try to compare by equality, you will not always get the expected result. 1.49012e-08 is a value very close to 0, but is still > 0.

The best thing to do in this situation is to avoid using floats for iterations, or to use a comparison function that has a certain margin of error. You can do it by using int for example is to calculate the "current" value by dividing by 10.

    
10.10.2017 / 01:08
0

Comparisons with floating-point numbers are hell! Avoid them!

In this case, you can avoid all these comparisons by pre-calculating the values within the desired range.

Increment and Decrement operations now work by moving the index position and reading the values contained in the array , using integer arithmetic:

#include <iostream>
#include <string>
#include <cmath>


class cVarFloat
{
    private:

        int         nPos;
        float       *pfArray;
        int         nArrayLen;
        float       fValueSet;
        float       fMax;
        float       fMin;
        float       fInitValue;
        std::string ItemText;

    private:

        void Initialize( void )
        {
            unsigned int i = 0;
            nArrayLen = static_cast<int>( std::ceil( (fMax - fMin) / fValueSet) ) + 1;
            pfArray = new float[ nArrayLen ];
            for( float f = fMin; f <= fMax; f += fValueSet, i++ ){
                pfArray[i] = f;
                if( this->fInitValue == f )
                    this->nPos = i;
            }
        }

    public:

        cVarFloat( std::string ItemText, float fMin, float fMax, float fValueSet, float fInitValue ) :
            nPos(0), fValueSet(fValueSet), fMax(fMax), fMin(fMin), fInitValue(fInitValue), ItemText(ItemText) { this->Initialize(); }

        virtual ~cVarFloat() { delete [] pfArray; }

        float GetMax(void) { return this->fMax; }
        std::string GetItemText(void) { return this->ItemText; }
        float GetValue(void) { return this->pfArray[ this->nPos ]; }

        void DecValue(void) { if( this->nPos == 0 )  this->nPos = this->nArrayLen - 1; else this->nPos--; }
        void IncValue(void) { if( this->nPos == this->nArrayLen - 1 ) this->nPos = 0; else this->nPos++; }
};


int main(void)
{
    cVarFloat Teste( "Testando um float", 0.0f, 0.5f, 0.1f, 0.5f );

    for( int i = 0; i < 15; i++ )
    {
        std::cout << Teste.GetValue() << std::endl;
        Teste.DecValue();
    }

    for( int i = 0; i < 15; i++ )
    {
        std::cout << Teste.GetValue() << std::endl;
        Teste.IncValue();
    }

}

Output:

0.5
0.4
0.3
0.2
0.1
0
0.5
0.4
0.3
0.2
0.1
0
0.5
0.4
0.3
0.2
0.3
0.4
0.5
0
0.1
0.2
0.3
0.4
0.5
0
0.1
0.2
0.3
0.4
    
10.10.2017 / 06:08
0

Floating-point formats have precision limits and do not guarantee the exact representation of certain numbers. In the case of the simple precision format (float) the relative value of this precision limit is around 1e-7 and in the double precision format it is around 1e-15. Your code has performed a few algebraic operations on numbers expressed in simple precision format. As the operations accumulated rounding errors, in the end obtained values lower than the precision limit. Everything is working as expected.

To deal with this kind of situation you need to take two aspects into account: the first is to know the order of magnitude of the expected result. If you are dealing with numbers that are multiples of 0.5 and you expect a possible result to be 0.0 then a result of 1e-8 is clearly zero. To solve this problem it is enough to round the results taking into account the order of magnitude of the expected values. The second aspect is the propagation of rounding errors. Each algebraic operation on floating-point variables has a potential for introducing rounding errors. With each operation you perform, these rounding errors will accumulate. There are operations, such as division, that have the potential to blow up the value of the error. If you really need accuracy in the results then you need to analyze algebraic operations to avoid unnecessary spreading of rounding errors and predict the maximum error limit.

    
22.10.2017 / 21:50