Implicit Conversions in C++

Going into more depth on what is happening when a variable is used as a different type
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

3D art showing a sorcerer character
Ryan McCombe
Ryan McCombe
Posted

We've seen that if our functions specify a return type, they always need to return something. This includes returning something from every conditional branch, if we are using them.

However, our functions don't necessarily need to return something of the correct type - just something that can be converted to the correct type. In many cases, this conversion will be done for us automatically, by a process known as implicit conversion.

We could write code to do it - we'll see how to do explicit conversions soon. But so far, we've just been using something of type X (eg, an int) as if it were something of type Y (eg a float) and letting the compiler figure it out

Implicit Conversions with Variables

Data types in C++ can have special functions that allow values of that type to convert themselves to other data types. We'll see how to give our custom data types this ability in a later chapter.

For now, we'll focus on the built in types. In fact, we've already seen these implicit conversions in action when working with numbers. For example, we've been able to create a float from an int:

float MyNumber { 5 }; // 5.0

This works, because the integer data type includes a special function that implemented the logic to convert one of its objects (eg, 5) to an object of the float type (eg, 5.0)

This is not always possible. For example, the strings data type does not have the ability to convert its objects to floats.

Lets try:

string MyString { "Hello" };
float MyNumber { MyString };

This fails with an error message similar to:

No viable conversion from `std::string` to `float`

Implicit Conversions with Functions

Implicit conversions apply to our function return types too. We should generally try to return the correct type, but it's not always necessary.

We should, of course, strive not to write code like this, but these are all acceptable C++ functions:

// Converting 0 to a boolean results in false
bool IsDeadFrom0() {
  return 0;  // false
}

// Converting a non-0 integer to a boolean results in true
bool IsDeadFrom42() {
  return 42; // true
}

// Converting false to an integer results in 0
int LevelFromFalse() {
  return false; // 0
}

// Converting true to an integer results in 1
int LevelFromTrue() {
  return true; // 1
}

// Conversions can happen multiple times
// This function will convert 50 to true...
bool GetHealth() {
  return 50;
}

// ...and this variable initialisation converts true to 1
int Health { GetHealth() }; // Health will be 1

// Operators also behave like functions
// This will result in 6
int Level { 5 + true };

C++ Narrowing Casts

Implicit casts are quite contentious. Many argue that programming languages shouldn't do them at all, because they're a source of errors and confusion.

We may, for example, write code like the following example:

int pi = 3.14;
float Circumference(float radius) {
  return 2 * pi * radius;
}

Our intent here is clearly to set pi to 3.14 but because we accidentally set the type to int rather than float, we'll actually be using 3.

The compiler will allow this, and we may not even notice that our calculations are are less accurate than we intended.

This is an example of a narrowing cast.

Narrowing casts are operations where converting one data type to another could cause loss of data. For example, storing 3.14 as an int would cause the .14 component to be lost, leaving us with only 3.

One advantage of Uniform Initialisation using { and } is that it protects us against this.

If we update our previous example to use uniform initialisation, the compiler will fail and alert us to the issue. This is better than letting a potential bug slip through:

int pi { 3.14 };
float Circumference(float radius) {
  return 2 * radius * pi;
}

Our compiler will tell us where the problem is, with error that will state something like "type double cannot be narrowed to int"

Here are some more examples:

float GetPiFloat() { return 3.14; }
double GetPiDouble() { return 3.14; }

// Converting a float to an int will fail
int LevelC { GetPiFloat() };

// Converting a double to an int will fail
int LevelD { GetPiDouble() };

// Converting a double to a float will fail
float LevelE { GetPiDouble() };

// But converting a float to a double is fine
// Any float can be converted to a double without data loss
double LevelF { getPiFloat() };

But Haven't we Been Narrowing Doubles This Whole Time?

In an earlier note, when talking about literals, we pointed out that an expression like 4.0 is actually a double, rather than a float.

We've also seen how a statement like the following is totally valid:

float MyNumber { 4.0 };

This may seem to conflict with what we just read. Isn't that code narrowing a double to a float?

The reason the above code works is that the compiler can determine at build time if the double 4.0 can be stored as the float 4.0f without data loss. It can, so the compiler allows it.

4.0 is an example of a constant expression. We have a dedicated lesson on this coming later in this course.

For now, just note that if 4.0 was the value contained in a variable, or the return of a function, it would no longer be a constant expression.

The following code shows an example of that.

double MyDouble { 4.0 };
float MyFloat { MyDouble };

After this small change, line 2 generates the same narrowing cast error we described in this section:

non-constant-expression cannot be narrowed from type `double` to `float`

It is, of course, possible to do conversions that result in data loss, like converting an int to a bool. Uniform initialisation just prevents it from being done automatically, because there's a risk we won't notice it's happening.

The compiler would rather we be explicit in our intentions. We'll be introduced to explicit casting soon.

Up next, we will add the final big piece to our initial exploration of functions. Arguments allow our functions to be even more dynamic and powerful.

Was this lesson useful?

Ryan McCombe
Ryan McCombe
Posted
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

3D art showing a progammer setting up a development environment
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, unlimited access!

This course includes:

  • 66 Lessons
  • Over 200 Quiz Questions
  • Capstone Project
  • Regularly Updated
  • Help and FAQ
Next Lesson

C++ Function Arguments

Making our C++ functions more useful and dynamic by providing them with variables to use in their execution
3D art showing two birds having an argument
Contact|Privacy Policy|Terms of Use
Copyright © 2023 - All Rights Reserved