Class Conversions
Here’s the next topic on classes: Conversions
Conversion Constructors
Remember that a class constructor with a single argument performs a type conversion?
Here’s a simple example:
//
// Kilometer.h
// Length Conversion
//
// Created by Bryan Higgs on 8/16/24.
//
#ifndef Kilometer_h
#define Kilometer_h
class Kilometer
{
public:
Kilometer(const double lengthInKilometers)
{
m_length = lengthInKilometers;
}
void print();
private:
double m_length;
};
#endif /* Kilometer_h */The simple class Kilometer contains
- A single constructor — a conversion constructor that converts a double value to a
Kilometer - A print method that prints the length of the
Kilometerinstance - A private data member,
m_length, that holds theKilometerinstance length value
//
// Kilometer.cpp
// Length Conversion
//
// Created by Bryan Higgs on 8/16/24.
//
#include <iostream>
#include "Kilometer.h"
void Kilometer::print()
{
std::cout << m_length << " kilometers\n";
}The implementation of the Kilometer::print method.
//
// main.cpp
// Length Conversion
//
// Created by Bryan Higgs on 8/16/24.
//
#include <iostream>
#include "Kilometer.h"
int main(int argc, const char * argv[])
{
Kilometer k1(10.5);
k1.print();
Kilometer k2(35.2);
k2.print();
return 0;
}A simple test program that creates two instances of Kilometer, and prints out their lengths:
10.5 kilometers
35.2 kilometers
Program ended with exit code: 0
Where Implicit Conversions are Used
User-defined conversions are used, in addition to standard conversions, for conversion of:
- initializers
- function arguments
- function return values
- expression operands
- expressions controlling iteration (while, do, for) and selection statements (if, switch)
- explicit type conversions.
Conversion by Constructor
Here’s an example:
//
// weights.h
// Weight Conversion
//
// Created by Bryan Higgs on 8/16/24.
//
class Ounce; // forward declaration
// WHY?
class Gram
{
public:
Gram(const double w) { m_weight = w; }
Gram(Ounce &oz);
double getWeight() { return m_weight; }
void print();
private:
double m_weight; // in grams
};
class Ounce
{
public:
Ounce(const double w) { m_weight = w; }
Ounce(Gram &gm);
double getWeight() { return m_weight; }
void print();
private:
double m_weight; // in ounces
};
const double GramsPerOunce = 28.349523;
inline Gram::Gram(Ounce &oz)
{
m_weight = oz.getWeight() * GramsPerOunce;
}
inline Ounce::Ounce(Gram &gm)
{
m_weight = gm.getWeight() / GramsPerOunce;
}Contains two classes: Gram, with:
- Conversion constructor from value double
- Conversion constructor from Ounce
- public accessor method, getWeight()
- public method print()
- private data member to hold the weight value in grams
and Ounce, with:
- Conversion constructor from double value
- Conversion constructor from Gram
- public accessor method, getWeight()
- public method print()
- private data member to hold the weight value in ounces
Plus:
- A const data value GramsPerOunce, for conversion between Grams and Ounces
- An inline definition of the Gram(Ounce) constructor
- An inline definition of the Ounce(Gram) constructor
QUESTION: Why did I have to define these inline constructors here, rather than inside their respective class declarations?
QUESTION: Why did I define Gram(Ounce &oz) and Ounce(Gram &gm), rather than Gram(const Ounce &oz) and Ounce(const Gram &gm)?
//
// weights.cpp
// Weight Conversion
//
// Created by Bryan Higgs on 8/16/24.
//
#include <iostream>
#include "weights.h"
void Gram::print()
{
std::cout << m_weight << " grams";
}
void Ounce::print()
{
std::cout << m_weight << " ounces";
}Contains the implementation for Gram::print() and Ounce::print().
QUESTION: Why did I move these to a .cpp file?
//
// testWeights.cpp
// Weight Conversion
//
// Created by Bryan Higgs on 8/16/24.
//
#include <iostream>
#include "weights.h"
int main()
{
Ounce one_ounce(1.0);
Gram one_gram(1.0);
Ounce o(one_gram);
Gram g(one_ounce);
one_ounce.print();
std::cout << " = ";
g.print();
std::cout << std::endl;
one_gram.print();
std::cout << " = ";
o.print();
std::cout << std::endl;
return 0;
}Here’s a test program using these classes:
It outputs:
1 ounces = 28.3495 grams
1 grams = 0.035274 ounces
Program ended with exit code: 0
Problems with Implicit Conversions
But imagine you have a Stack class:
//
// Stack.h
// Implicit Conversion Problem
//
// Created by Bryan Higgs on 8/17/24.
//
#ifndef Stack_h
#define Stack_h
class Stack
{
// ...
public:
Stack(int size) // stack size
{
p = new int[size];
}
~Stack()
{
delete [] p;
}
private:
int *p; // Pointer to stack of ints
};
#endif /* Stack_h */Note that the intention of the Stack(int size) is to construct an instance of a stack, allocating that many items from the heap.
~Stack() will deallocate the stack from the heap.
… and then you try to use it like this:
//
// main.cpp
// Implicit Conversion Problem
//
// Created by Bryan Higgs on 8/17/24.
//
#include "Stack.h"
void DoIt(Stack s)
{
// Do something...
}
int main()
{
Stack s(30);// stack of 30 ints
DoIt(4); // what happens here?
return 0;
}What will happen when DoIt() is invoked with a value of int?
When I annotated the Stack constructor and destructor with debugging outputs, it gave me:
Instance of Stack constructed with size: 30
Instance of Stack constructed with size: 4
Instance of Stack being destroyed
Instance of Stack being destroyed
Program ended with exit code: 0
Can you explain this? What is happening? Is this a good idea?
Clearly, implicit conversions can be problematic. Such implicit silent happenings behind the scenes are not necessarily a good idea.
How to Avoid Implicit Conversions
Here are some possibilities:
- Don’t declare a constructor with a single argument.
- Not always practical!
- Use the explicit keyword on the constructor declaration to prevent implicit usage. (At the time of writing this was a relatively new feature of C++)
- Substitute a special class as a ‘stand-in’ for the desired single argument.
- It’s a little long-winded.
- But, it can be good data abstraction!
- Use a static member function to explicitly ‘make’ an instance of the class
- we’ll discuss this later…
The explicit Keyword
//
// Stack.h
// Implicit Conversion Problem
//
// Created by Bryan Higgs on 8/17/24.
//
#ifndef Stack_h
#define Stack_h
class Stack
{
// ...
public:
explicit Stack(int size) // stack size
{
p = new int[size];
}
~Stack()
{
delete [] p;
}
private:
int *p; // Pointer to stack of ints
};
#endif /* Stack_h */We made one addition to the original Stack.h file:
- Added the ‘explicit’ keyword to the Constructor declaration.
//
// main.cpp
// Implicit Conversion Problem
//
// Created by Bryan Higgs on 8/17/24.
//
#include "Stack.h"
void DoIt(Stack s)
{
// Do something...
}
int main()
{
Stack s(30);// stack of 30 ints
DoIt(4); // what happens here?
return 0;
}I made no changes in the main.cpp program, but…
Now, the C++ compiler issues an error for line 18:
No matching function for call to ‘DoIt’
In other words, the implicit conversion has been suppressed.
Declaring a ‘Stand-in’ Class
Here’s an example of how to use a ‘stand-in’ class to avoid the problem:
//
// Stack.h
// Conversion Stand-in Class
//
// Created by Bryan Higgs on 8/17/24.
//
#ifndef Stack_h
#define Stack_h
class StackSize
{
int s;
public:
StackSize(int size)
{ s = size; }
int Size()
{ return s; }
};
class Stack
{
// ...
public:
Stack(StackSize &size) // stack size
{
p = new int[size.Size()];
}
~Stack()
{
delete [] p;
}
private:
int *p; // Pointer to stack of ints
};
#endif /* Stack_h */This time, we added a class StackSize, which ‘stands in’ for the size of the stack, in place of a simple int.
NOTE: We must use a Stack constructor of
Stack(StackSize &size)
because, if we used a constructor like:
Stack(StackSize size)
it would pass size as a value, and thus create an implicit copy.
Here’s the original program again, with some changes to use the StackSize class…
//
// main.cpp
// Implicit Conversion Problem
//
// Created by Bryan Higgs on 8/17/24.
//
#include "Stack.h"
void DoIt(Stack s)
{
// Do something...
}
int main()
{
StackSize size1(30);
Stack s(size1);// stack of 30 ints
StackSize size2(4);
Stack s2(size2); // stack of 4 ints
DoIt(s2); // what happens here?
return 0;
}Now, the compile-time errors have disappeared…
BUT:
When we run the program, we get:
malloc: *** error for object 0x600000010020: pointer being freed was not allocated
The
delete [] p;
in the Stack destructor is trying to delete an array that doesn’t belong to the original Stack instance being passed.
It turns out that this was because we are passing the Stack parameter to DoIt by value.
That means that the compiler generates code to create an implicit copy, and that copy is not a true instance of Stack.
See, later, when we talk about copy constructors.
//
// main.cpp
// Implicit Conversion Problem
//
// Created by Bryan Higgs on 8/17/24.
//
#include "Stack.h"
void DoIt(Stack &s)
{
// Do something...
}
int main()
{
StackSize size1(30);
Stack s(size1);// stack of 30 ints
StackSize size2(4);
Stack s2(size2); // stack of 4 ints
DoIt(s2); // what happens here?
return 0;
}Simply changing the one parameter of DoIt to pass by reference fixes the problem.
Shortcomings of Conversion Constructors
A conversion constructor, by definition, converts a class or type into an instance of its own class. For example :
Complex(double); // 'convert' double to Complex
But conversion constructors:
- Cannot define a conversion to a base type (like int or double)
- Cannot define a conversion from one class to another without modifying the declaration for the other (target) class.
So what to do?
Conversion Functions
A conversion function (a.k.a. conversion operator) is a class member function that looks like:
class X
{
// ...
operator T() const; // Conversion function from X to T
// ...
};
It converts from its class (X in this case) to the type following the operator keyword (T in this case).
Conversion functions have no arguments, and the return type is implicitly the target conversion type.
Here’s an example:
//
// Integer.h
// Conversion Functions
//
// Created by Bryan Higgs on 8/18/24.
//
#ifndef Integer_h
#define Integer_h
// Integer.h
class LargeInteger
{
public:
LargeInteger(long l) { m_l = l; }
operator long() const // LargeInteger to long
{
return m_l;
}
private:
long m_l;
};
class Integer
{
public:
Integer(int i) { m_i = i; }
operator int() const // Integer to int
{
return m_i;
}
operator LargeInteger const() // Integer to LargeInteger
{
return LargeInteger(m_i);
}
private:
int m_i;
};
#endif /* Integer_h */Here are two classes, LargeInteger, and Integer which define a number of conversion functions.
Here’s a main program to test the conversions:
//
// main.cpp
// Conversion Functions
//
// Created by Bryan Higgs on 8/18/24.
//
#include "Integer.h"
int main(int argc, const char * argv[])
{
Integer i(5);
int intValue = 10;
intValue = i;
LargeInteger value = i;
LargeInteger l1(10);
long value2 = l1;
LargeInteger l2(i);
return 0;
}Conversion Ambiguities
Consider:
//
// Complex.h
// Conversion Ambiguities
//
// Created by Bryan Higgs on 8/18/24.
//
#ifndef Complex_h
#define Complex_h
class Complex
{
// ...
public:
Complex(short i);
Complex(long l);
// ...
};
#endif /* Complex_h */This is ambiguous, since the compiler doesn’t know which implicit conversion to apply (int to short, or int to long)
//
// main.cpp
// Conversion Ambiguities
//
// Created by Bryan Higgs on 8/18/24.
//
#include <iostream>
#include "Complex.h"
int main()
{
Complex c(1); // Call to constructor of 'Complex' is ambiguous
// ...
return 0;
}Summary
Now, we know a little more about classes. In particular, we know more about:
- Conversion constructors
- Conversion functions (operators)
- Conversion ambiguities
But there’s still lots more to learn about classes…