Implicit Conversions and Narrowing Casts

Going into more depth on what is happening when a variable is used as a different type

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 - for example, what happens when we use an int where a float is expected?

This scenario introduces us to the concept of 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'll explore how implicit conversions 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

Just like with variables, C++ can automatically convert values when they're returned from functions. While it's best practice to return the exact type a function declares, C++ will perform implicit conversions when necessary.

Here are some examples of implicit conversions with function returns:

// 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

While C++ allows these conversions, it's better to return the exact type your function declares. This makes your code clearer and helps prevent unexpected behavior.

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 as values returned from functions:

// 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. Note, we're using the = syntax to initialize this variable, rather than the { } syntax we typically prefer:

#include <iostream>
using namespace std;

int main() {
  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()};

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.

In those scenarios, the compiler would rather we be explicit in our intentions. We'll learn how to do explicit conversions soon.

Summary

In this lesson, we explored 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.

Next Lesson
Lesson 14 of 60

Function Arguments and Parameters

Making our functions more useful and dynamic by providing them with additional values to use in their execution

Questions & Answers

Answers are generated by AI models and may not have been reviewed. Be mindful when running any code on your device.

Testing Type Conversions
Is there a way to check what value a type will be converted to before using it?
Converting Large Numbers
What happens if I try to convert a really big number to a smaller type?
Numbers as Booleans
Why does C++ treat non-zero numbers as true and zero as false?
Performance Impact
Are implicit conversions slower than using exact types?
Purpose of Implicit Conversions
Why do we need implicit conversions at all? Wouldn't it be safer to always require exact types?
Disabling Implicit Conversions
Can I prevent the compiler from doing any implicit conversions in my code?
Language Comparison
What's the difference between how C++ handles conversions versus other languages?
Memory Usage
How do implicit conversions affect memory usage in my program?
Or Ask your Own Question
Get an immediate answer to your specific question using our AI assistant