Implicit Conversions and Narrowing Casts

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

Free, Unlimited Access
3D art showing a sorcerer character
Ryan McCombe
Ryan McCombe
Updated

In our journey so far, we have discovered how functions and variables work with specific data types. Each type, like int or float, plays a unique role. But what happens when we mix these types, perhaps using an int where a float is expected?

This scenario introduces us to a crucial concept in C++: implicit conversion.

It allows us to use different types of values interchangeably in some situations, without needing to manually convert them. For instance, using an int value where a float is needed. The compiler, our code's translator, handles this conversion for us.

However, there's a twist. Not all conversions are equal. Some are straightforward, like turning an int into a float. Others, like converting a string to an int, are not possible implicitly.

In this lesson, we will dive deep into the world of implicit conversions, exploring how they work with variables, functions, and operators. We'll also tackle the concept of narrowing casts, a special type of conversion that needs extra attention.

Implicit Conversions with Variables

Data types in C++ can allow values of their type to be created from a value of a different type. 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. 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 float data type includes the ability to create one of its objects (eg, 5.0) using an object of the int type (eg, 5)

This is not always possible. For example, the float data type cannot create objects from a string. Let's 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;
}

// This variable initialization
// converts true to 1
int Health { GetHealth() }; // Health will be 1

Implicit Conversions with Operators

Behind the scenes, operators are very similar to functions. We’ll see examples of this later in the course, where we implement our custom operators.

Because of this, operators also implement implicit conversions in the same way we covered above:

// This will result in 6
int Level { 5 + true };

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;

Our intent here is almost certainly to initialize Pi to 3.14 but because we accidentally set the type to int rather than float, we'll be using 3.

The compiler will allow this, and we may not even notice that our calculations 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.

Uniform Initialisation Prevents Narrowing Casts

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

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

int Pi { 3.14 };

Our compiler will tell us where the problem is, with an error:

Error: conversion from 'double' to 'int'
requires a narrowing conversion

Here are some slightly more complex examples:

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

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

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

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

// But converting a float to a double is fine
// Any float can be converted to a double
// without data loss
double D{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 a double, rather than a float.

We've also seen how a statement like the following is 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. that is, something that can be evaluated at compile time. 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, we get the same narrowing cast error we described in this section:

error: 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 initialization 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.

Summary

In this lesson, we delved into the concepts of implicit conversions and narrowing casts. Here's what we covered:

  • Implicit Conversions: Understanding how C++ automatically converts one data type to another when needed, such as converting an int to a float.
  • Implicit Conversions with Variables and Functions: Seeing how built-in data types allow creation from different types, and applying this knowledge to functions and operators.
  • Narrowing Casts: Learning about conversions that can lead to data loss, like converting a double to an int.
  • Uniform Initialization: Exploring how this syntax helps prevent unintentional narrowing casts, preventing potential bugs.

We hope this lesson has shed light on how implicit conversions and narrowing casts work in C++, enhancing your understanding of the language's versatility and potential pitfalls.

Preview of Next Lesson

As we wrap up our exploration of implicit conversions and narrowing casts, we set our sights on the next topic: Function Parameters and Parameters. Here's a sneak peek of what we'll dive into:

  • Defining Functions with Parameters: Learn how to create functions that can take inputs, allowing for more dynamic and reusable code.
  • Passing Arguments: Understand how to pass data to functions, exploring the relationship between arguments and parameters.
  • Different Types of Parameters: Discover the various ways to define parameters in functions, including default values and why they are useful.

Was this lesson useful?

Next Lesson

Function Arguments and Parameters

Making our functions more useful and dynamic by providing them with additional values to use in their execution
3D art showing two birds having an argument
Ryan McCombe
Ryan McCombe
Updated
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
Functions, Conditionals and Loops
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:

  • 56 Lessons
  • Over 200 Quiz Questions
  • 95% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Function Arguments and Parameters

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