Jump to content
  • Advertisement
Sign in to follow this  
suliman

C++ Avoiding "similar conversion" errors?

This topic is 481 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

Hi!

I have two simple functions like this:

bool within(int var, int min, int max){
if (var>=min && var<=max)
return true;
else
return false;
}

bool within(float var, float min, float max){
if (var>=min && var<=max)
return true;
else
return false;
}

Calling it like this gives me "Error    2    error C2666: 'within' : 2 overloads have similar conversions

float foo = 0.4;

if(within(foo, 0, 1))
   bar();

I know i can specify constants to be float like so : (within(foo, 0f, 1f)) but i remember I earlier didnt have to do this and I use this ALL over in my projects and now suddenly has loads of compilation errors.

Can I write this in some way that it just accepts (float, something, something) and that would use the second function? (the one that works with floats?)

Thanks!
Erik

Edited by suliman

Share this post


Link to post
Share on other sites
Advertisement

My gut says to fix the compilation errors. Int <-> float coercion is evil and you should not rely on it. More strongly, you should be scared shitless of it and hate the possibility that it might bite you someday - because it just did.

You can simplify your function to:

template <typename T>
bool within (T value, T min, T max) {
  return (value >= min) && (value <= max);
}

But I don't think that actually eliminates the silent int/float coercion so it's more a side issue.

The root problem is that you are passing a float and two ints to your function, which is defined to take either three floats or three ints. The conversion that the language allows between float and int makes this call ambiguous. The correct solution is again to fix the passing of ints when you want to check a float. The easiest change is to suffix your literals with f, as you noted. The harder and arguably messier change is to cast explicitly.

Share this post


Link to post
Share on other sites
15 minutes ago, suliman said:

within(foo, 0, 1)

You've got parameters (float, int, int) there.  The compiler needs to do some type conversion to make it work.

The problem is that you've got more than one valid conversion, and there is no requirement the compiler can use to pick one over the other.

Even if you go with template versions you're still mixing types, the (var>=min && var<=max) is doing an operation between the first and second parameter and the first and third parameter so the types need to have compatible operations.  If it were a template function then the rules became a little more strict for the inequality operations (greater than, less than) in C++11 and C++14 but it would still have the same fundamental issue in older compilers.

 

There are a series of implicit conversions that the compiler can do automatically, but in this case it is ambiguous: Is it supposed to turn the float into an int, or turn the int into a float? Conversions in either direction can potentially lose information. Neither one is inherently right or wrong as far as the compiler is concerned. The compiler has a long list of potential conversions and two of them are both valid.

The details of that floating/integral conversion are implementation defined.  It could convert them to int, it could convert them to float, it could generate a warning or not, all of that is implementation defined.  

31 minutes ago, suliman said:

but i remember I earlier didnt have to do this and I use this ALL over in my projects and now suddenly has loads of compilation errors.

That's an implementation decision. Through history most compilers would warn by default, but different warning levels could be used that would enable or disable the warning. 

For your old code, old versions of Visual C++ were more lax about many conversions, silently choosing conversions that may lose precision or deciding between versions when they were equal in precedence. In particularly, VC++ before 2008 were far more lax about several of the standard-required conversions. The code was technically ambiguous but the standard allows for implementation defined behavior. The compiler made some implementation-defined choices about which conversions to take and accepted it without generating a warning.  As the compiler teams in VC++2005 and VC++2008 pushed for better standards compliance there were many of these errors that started popping up in some code bases.

The warning there is correct, pass all three as the same parameter type.  If that means using floating point constants like 0f or 1f, then do it, just as in the 16-bit world people would write constants like 0L or 1L, or in today's world many code standards encourage 0ULL or 1LL and similar.  

Make sure your parameters are the correct type so the compiler doesn't have to guess.

 

Share this post


Link to post
Share on other sites

Thanks!

Is there any way to let the first parameter "decide" the format of the others? In this case make the first one the "master" and cast the other ones accordingly? I mean i can write "float foo = 2; " The 2 is a integer (acoording to the logic that causes problem in my code above) but it doesnt complain about needing to cast it to a float in that line. Can I use some similar technique in the code above?

Share this post


Link to post
Share on other sites

You could always use something like this.

template<typename T, typename R>
  bool
  within(T value, R min, R max)
  {
  	return (min <= value) && (value <= max);
  }

If you frequently find yourself comparing, say, complex numbers to strings (since pretty much anything would go here), you can add some static_asserts and type classifiers to indicate type errors at compile time.

Frankly, you're better off making type conversions explicit for the reader and just using either the right constants or casting in the call line.

Share this post


Link to post
Share on other sites

That still suffers the same problem.  The comparison (a <= b) for (float <= int) is still going to result in the same question:  Should the compiler convert the float to an int, or an int to a float?  (The rules are well-defined, but may not be the one you are expecting. ) Both options are exactly one floating-integral conversion so both remain equally valid. The compiler will do the conversion it believes is correct through implicit conversions, which may not be the one you believe to be correct. It is still better to explicitly pick the correct value rather than rely on an implicit conversion.

I suppose yet another option is to explicitly provide those options, but it seems even worse to me:

bool within(float v, int a, int b) { return within( v, (float)a, (float)b);}

bool within( float v, int a, float b) {...}

bool within( float v, float a, int b) {...}

Explicit is better than implicit in cases like this.  If you want the float version pass floats. If you want the int version pass ints. Don't mix and match with the hope that implicit conversions will convert the way you want.

Share this post


Link to post
Share on other sites

Thanks for clearing that up. On a side note, is the template only "available" for the next function/brackets section?

template <typename T>
bool within(T var, T min, T max){
	return (var >= min && var <= max);
}
      
template <typename P>
bool withinTest(T var, T min, T max){
	return (var >= min && var <= max);
}                                   

If i write like this, the second function is not aware of "T" and cannot compile.

 

Share this post


Link to post
Share on other sites

The words are a little rough, but I think the answer is yes.

The template portion is a part of the total signature.  People commonly write it on a second line, but that is just for readability.  You could put every word on a new line, or squish the code down to a single line of text, the language doesn't handle it differently.  

The first template is:  template <typename T> bool within (T var, T min, T max) {...}

Defines a single template, basically a cookie cutter, with T as a replacement.  When the function is encountered, all the options for template types "T" are tried as replacements for the symbol "T", the best match is found, and new function is automatically generated where "T" is replaced with the template type.

That is the entire definition of the template, it stops at the end of the function body.

The second template is: template <typename P> bool withinTest(T var, T min, T max){...} 

Defines a second template, basically a cookie cutter, with P as a replacement.  When the function is encountered all the options for type "P" are tried as replacements for the symbol "P". Unfortunately the compiler cannot find any type called "T", so compilation will fail.

Again, that is the entire definition, it stops at the end of the function body.

A class template is: template <typename A> class foo {...} 

Defines a class template, basically a cookie cutter, with A as a replacement. When an instance is created all the options for type "A" are tried as replacements for the symbol "A", etc. It stops at the end of the class body.

 

Both template classes and template functions start with the word "template", then some type parameters inside a < > block, then provides the rest of the class or function that is basically a search-and-replace with that symbol replaced.  

I think of it more like a cookie cutter rather than actual code. You make the cookie cutter in the right shape. The cookie maker tries it on all the options, perhaps trying it on gingerbread cookies,  sugar cookies,  oatmeal cookies,  vanilla cookies, wafer cookies, and all other cookies it has ever learned about, eventually finding the version that works the best.  On rare occasion you will expect there to be one match, perhaps expecting apple-cinnamon flat cookies, only to discover the choice was chocolate pistachio sea salt cookies that you never thought was an option.

Similarly, template code takes a long time to compile because it has to consider all the options (and sometimes "ALL the options" is hundreds of thousands of types, references to types, pointers to types, typedefs of types, parameter packs, enumeration types, deduced types ...) and considering all the options takes time. In rare cases the best option the compiler finds is not the one you would have thought, which may be an unexpected neutral surprise or an unexpected defect.


Also somewhat critically, something many people forget is that templates are not actual code. One excellent writeup is on cppreference: "A template by itself is not a type, or a function, or any other entity. No code is generated from a source file that contains only template definitions. In order for any code to appear, a template must be instantiated: the template arguments must be determined so that the compiler can generate an actual function or an actual class."  The template tells the compiler how to generate a function with the proper types.  This subtlety helps explain many of the quirky behaviors templates seem to have.

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!