CS202 Programming

Systems

 

Lecture Notes #8

 

 

 

User Defined Conversions

 

 

 

 

 

Lecture and Reference

 

 

 

 

User Defined Type Conversions: Conversions to or from a user defined type, specified by a class.

The following sections describe how to specify user defined type conversions for classes we define. Conversions can be specified from a built-in type to a class we define, from a class we define to another class, and from a class we define to a built-in type. These conversions can then be used implicitly by the compiler as needed or explicitly by the client. The following sections describe when such conversions are needed, how to specify these conversions, and potential problems that can occur and how to avoid them.

 

8.1 Use of Type Conversions

Implicit User Defined Conversions: Conversions that occur when mixed type expressions are evaluated or when actual arguments do not match the formal argument types.

Explicit User Defined Conversions: Occur when the client explicitly invokes a cast operation.

Type conversions are used when an object of one type is used in a context in which an object of another type is expected. Such conversions can occur implicitly or explicitly.

Implicit conversions occur when mixed type expressions are evaluated or when the actual arguments in a function call do not match the formal arguments of the function prototype. In such cases, the compiler implicitly applies a conversion from one type to another. The conversion rules are summarized here for convenience. First, the compiler attempts to apply a trivial conversion. If there is no match applying a trivial conversion, then the compiler attempts to apply a promotion. If there is no match applying a promotion, then the compiler attempts to apply a built-in type conversion. If there is no match applying a built-in type conversion, then the compiler attempts to apply a user defined type conversion. Finally, if there is no match from applying a user defined type conversion, the compiler generates an error.

In order to determine if a user defined type conversion can be used, the compiler first checks that there is a conversion defined for the type needed. If there is, then the compiler checks that the type required by the conversion matches the type supplied or that the type supplied can be converted to the type required by applying one of the built-in type promotions or conversions. Only one such built-in type promotion or conversion will be applied to the type required by a user defined type conversion before applying the user defined type conversion itself. Thus, at most one built-in type conversion and at most one user defined type conversion will ever be implicitly applied when converting from one type to another.

Explicit conversions occur when the client explicitly invokes a cast operation. Remember, there are three ways to perform explicit type casting in C++: the type cast operator, the functional notation, and the keyword notation. Explicit type conversion allows our programs to control when type conversions take place instead of relying on such conversions to occur implicitly.

Both implicit and explicit type conversions result in a temporary object of the type being converted to. This temporary object is used in place of the original object. The value of the original object is not affected by the conversion. When considering execution efficiency, it is important to reduce or minimize the creation of such temporary objects. We recommend minimizing user defined conversions as much as possible by avoiding mixed type expressions and by supplying arguments to functions that exactly match the type required by the function prototypes.

 

8.2 Using the Constructor as a Type Conversion Function

Constructors taking a single argument define a conversion from the type of its argument to the type of its class. Such conversion functions can be used either implicitly or explicitly. When used implicitly, at most one implicit built-in promotion or conversion will be applied to the argument of the constructor.

The following example illustrates the use of a constructor as a conversion function. This example has incorporated output statements in the constructor and destructor to better see the creation of temporary objects:

 

//name.h interface (Ex1211)

class name {

  public:

    name(char* = "");       //default constructor

    ~name();                //destructor

    const char* get_name(); //get pointer to name

  private:

    char array[32];         //array containing name

};

 

//name.cpp implementation (Ex1211)

#include <iostream>

#include <cstring>

using namespace std;

#include "name.h"

 

name::name(char* string) {     //constructor

  cout <<"name constructor called" <<endl;

  strncpy(array, string, 32);  //copy name into array

}

 

name::~name() {                //destructor

  cout <<"name destructor called" <<endl;

}

 

const char* name::get_name() { //get pointer to name

  return(array);

}

 

//main.cpp (Ex1211)

#include <iostream>

using namespace std;

#include "name.h"

 

void function(name);

 

int main() {

  name obj;

 

  obj = "sue smith";     //implicitly convert char* to name

  cout <<obj.get_name() <<endl;

 

  function("sue smith"); //implicitly convert char* to name

  return (0);

}

 

void function(name obj) {

  cout <<obj.get_name() <<endl;

}

 

The following is the output from this program:

 

name constructor called  //creation of obj

name constructor called  //creation of temporary

name destructor called   //destruction of temp after assign

sue smith

name constructor called  //creation of temporary

sue smith

name destructor called   //destruction of temp after call

name destructor called   //destruction of obj

 

Remember, the lifetime of a temporary object is from the time it is created until the end of the statement in which it was created. In the assignment statement, the temporary object is destroyed after the temporary is assigned. As a formal argument (passed by value), the temporary object is destroyed after the function is called.

If we do not want a constructor taking a single argument to also define an implicit conversion function, we can prevent that by preceding the constructor declaration with the keyword explicit. The following example has been modified to make the constructor explicit. The client is now required to provide explicit type casts in order to convert a char* to a name object.

 

//name.h interface (Ex1212)

class name {

  public:

    name(char* = "");       //default constructor

    ~name();                //destructor

    const char* get_name(); //get pointer to name

  private:

    char array[32];         //array containing name

};

 

//main.cpp (Ex1212)

#include <iostream>

using namespace std;

#include "name.h"

 

int main() {

  name obj;

 

  obj = (name)"sue smith";

  cout <<obj.get_name() <<endl;

 

  obj = name("sue smith");

  cout <<obj.get_name() <<endl;

 

  obj = static_cast<name>("sue smith");

  cout <<obj.get_name() <<endl;

  return (0);

}

 

Using a constructor as a conversion function allows us to convert an object of some other type to an object of a class. They do not allow us to define a conversion from a class to some other built-in type or class. To do so, we must define a type conversion function.

 

8.3 Defining a Type Conversion Function

A conversion function allows us to define a conversion from an object of a class to another built-in type. Conversion functions are also useful when we need to convert from an object of our class to an object of a class that we do not have access to or do not want to modify. When we do have access to the class and are willing to modify it, we can always define a constructor taking a single argument of our class type to perform the conversion.

The following is the syntax for declaring a conversion function that converts an object of a class to an object of some other type. This is placed within the class interface.

 

operator other_type();

 

Notice that this conversion function has no return type. That is because the return type is implicitly specified (other_type), since that is the type we are converting to. The conversion function converts the current object into the new type and returns the value. The conversion function can be called either implicitly or explicitly. The name of the conversion function must be a single identifier; therefore, for types that are not a single identifier, a typedef must first be used to create a single identifier.

The following example shows how to create and use a conversion function and how to invoke it either explicitly or implicitly:

 

//name.h interface (Ex1213)

class name {

  public:

    name(char* = "");          //default constructor

    ~name();                   //destructor

    typedef const char* pchar; //make single identifier

    operator pchar();          //conversion (name to char*)

    const char* get_name();    //get pointer to name

  private:

    char array[32];            //array containing name

};

 

//name.cpp implementation (Ex1213)

#include <iostream>

#include <cstring>

using namespace std;

#include "name.h"

 

name::name(char* string) {     //constructor

  cout <<"name constructor called" <<endl;

  strncpy(array, string, 32);  //copy name into array

}

 

name::~name() {                //destructor

  cout <<"name destructor called" <<endl;

}

 

name::operator pchar() {       //conversion function

  cout <<"conversion function pchar called" <<endl;

  return array;

}

 

const char* name::get_name() { //get pointer to name

  return(array);

}

 

//main.cpp (Ex1213)

#include <iostream>

using namespace std;

#include "name.h"

 

void function(const char*);

 

int main() {

  name obj("sue smith");

  const char* p;

 

  p = obj;       //conversion pchar implicitly called

  cout <<p <<endl;

 

  function(obj); //conversion pchar implicitly called

  return (0);

}

 

void function(const char* p) {

  cout <<p <<endl;

}

 

The following is the output from this program:

 

name constructor called

conversion function pchar called

sue smith

conversion function pchar called

sue smith

name destructor called

 

The conversion function can also be called explicitly using any of the three possible cast notations. In order for the conversion function to be called explicitly when it is a typedef name, that name must be in the global namespace and cannot be hidden within the class. In the following example, the typedef name has been put into the global namespace and is then used in each of the three cast notations:

 

//name.h interface (Ex1214)

typedef const char* pchar;  //make single identifier global

class name {

  public:

    name(char* = "");       //default constructor

    ~name();                //destructor

    operator pchar();       //conversion (name to char*)

    const char* get_name(); //get pointer to name

  private:

    char array[32];            //array containing name

};

 

//main.cpp (Ex1214)

#include <iostream>

using namespace std;

#include "name.h"

 

int main() {

  name obj("sue smith");

  const char* p;

 

  p = (pchar)obj;

  cout <<p <<endl;

 

  p = pchar(obj);

  cout <<p <<endl;

 

  p = static_cast<pchar>(obj);

  cout <<p <<endl;

  return (0);

}

 

8.4 Ambiguous Conversions

When we use constructors as conversion functions and also define conversion functions, we must be careful to avoid mutual conversions. This means that we must avoid the following when converting an object from class source to class destination:

 

destination::destination(source); //constructor conversion

source::operator destination();   //conversion function

 

Because of this is ambiguous, we will get a compiler error.

 

8.5  Implicit Conversions

Implicit user defined conversions are often used to reduce the number of overloaded operators that need to be implemented. The following section discusses when these implicit conversions can be helpful. We then discuss issues to be aware of when providing implicit user defined conversions in conjunction with overloaded operators.

 

8.5.1  Symmetric Operations

Symmetric operators, such as addition and subtraction, are best defined as non-members. The member form requires that the first operand be an object of the class. The non-member form does not require that an object of the class be the first operand. Therefore, for the non-member form, all combinations of argument types can be overloaded. For example, if we wanted to allow mixed mode expressions containing both complex and double types, we could only overload two of the three possible combinations as members. However, as non-members, we could overload all three combinations. The following example illustrates this:

 

//complex.h (Ex1305)

class complex {

  friend complex operator-(const complex &,

                           const complex &);

  friend complex operator-(const complex &, double);

  friend complex operator-(double, const complex &);

  public:

    explicit complex(double r=0, double i=0) :

      real(r), imag(i) { }

  private:

    double real; //real component

    double imag; //imaginary component

};

 

inline complex operator-(const complex &c1,

                         const complex &c2) {

  complex temp;

  temp.real = c1.real - c2.real;

  temp.imag = c1.imag - c2.imag;

  return (temp);

}

 

inline complex operator-(const complex &c1, double d) {

  complex temp;

  temp.real = c1.real - d;

  temp.imag = c1.imag - 0;

  return (temp);

}

 

inline complex operator-(double d, const complex &c2) {

  complex temp;

  temp.real = d - c2.real;

  temp.imag = 0 - c2.imag;

  return (temp);

}

 

//main.cpp (Ex1305)

#include "complex.h"

 

int main() {

  complex c(1.0, 2.0);

  double  d(3.14159);

 

  c = c - c; //okay if operator- is member or non-member

  c = c - d; //okay if operator- is member or non-member

  c = d - c; //not allowed if operator- is member

  return (0);

}

 

In the previous examples, we used the explicit keyword to prevent the constructor from being used as an implicit user defined conversion from double to complex. Without the explicit keyword, the constructor can be used as a conversion function. Then, only one non-member form of the operator needs to be provided where both arguments are of type complex. Whenever an arithmetic type is supplied as an operand, that type is promoted or converted to a double and then the implicit double to complex conversion is performed. The one overloaded operator- function is then defined with two arguments of type complex. The following example illustrates this:

 

//complex.h (Ex1306)

class complex {

  friend complex operator-(const complex &,

                           const complex &);

  public:

    complex(double r=0, double i=0) : real(r), imag(i) { }

  private:

    double real; //real component

    double imag; //imaginary component

};

 

inline complex operator-(const complex &c1,

                         const complex &c2) {

  complex temp;

  temp.real = c1.real - c2.real;

  temp.imag = c1.imag - c2.imag;

  return (temp);

}

 

//main.cpp (Ex1306)

#include "complex.h"

 

int main() {

  complex c(1.0, 2.0);

  double  d(3.14159);

 

  c = c - c; //no conversion necessary before call

  c = c - d; //double converted to complex before call

  c = d - c; //double converted to complex before call

  return (0);

}

 

The use of implicit user defined conversions can help to reduce the number of overloaded operators that must be implemented. However, they can produce subtle changes in the way a program works. For example, if we were to add a complex to double user defined conversion function to our complex class, all uses of operator- containing both complex and arithmetic types would become ambiguous. This is discussed in the next section.

Practical Rule:  We recommend that all arithmetic and bitwise binary operators be overloaded as non-members.

 

8.5.2  Surprises and Ambiguities

Consider the following implementation of the operator- function as a member of our complex class. In this example, we have provided a constructor that can take a single argument of type double. Such a constructor provides an implicit user defined conversion from a double to a complex. This allows us to use a mixed mode expression as in the following example:

 

//complex.h (Ex1307)

class complex {

  public:

    complex(double r=0, double i=0) : real(r), imag(i) { }

    complex operator-(const complex &) const;

  private:

    double real; //real component

    double imag; //imaginary component

};

 

inline complex complex::operator-(const complex &c) const {

  complex temp;

  temp.real = real - c.real;

  temp.imag = imag - c.imag;

  return (temp);

}

 

//main.cpp (Ex1307)

#include "complex.h"

 

int main() {

  complex c(1.0, 2.0);

  double  d(3.14159);

 

  c = c - d; //double converted to complex

  return (0);

}

 

So far, all is okay. However, if we switch the operands as shown in the next example, we will get a compile error. The first operand must be an object of our class. Even though we have provided a user defined conversion from a double to a complex, it is not applied to the first operand when the operator is implemented as a member.

 

//main.cpp (Ex1308)

#include "complex.h"

 

int main() {

  complex c(1.0, 2.0);

  double  d(3.14159);

 

  c = d - c; //ERROR-first operand must be complex

  return (0);

}

 

Now suppose we add a user defined conversion function converting complex to double. We no longer get a compile error. Instead, our program compiles and executes but uses the incorrect (built-in) subtraction operator instead of the one we overloaded! Consider the following example:

 

//complex.h (Ex1309)

class complex {

  public:

    complex(double r=0, double i=0) : real(r), imag(i) { }

    complex operator-(const complex &) const;

    operator double();

  private:

    double real; //real component

    double imag; //imaginary component

};

 

inline complex complex::operator-(const complex &c) const {

  complex temp;

  temp.real = real - c.real;

  temp.imag = imag - c.imag;

  return (temp);

}

 

inline complex::operator double() {

  return real;

}

 

//main.cpp (Ex1309)

#include "complex.h"

 

int main() {

  complex c(1.0, 2.0);

  double  d(3.14159);

 

  c = d - c; //uses two implicit user defined conversions

  return (0);

}

 

In order to understand the previous example, we must consider operator overload resolution. Remember from Section 13.5.4 that the candidate operator functions can consist of non-members, members, and built-in candidates. In this example, there is only one member candidate and five built-in candidates. The member candidate is not viable because the first operand is not a class type. That leave us with only the built-in candidates as viable functions. Since the first operand is a double and the second operand must be converted from complex to double, the best viable function is the subtraction operator taking operands of type double. The residual value of the expression is therefore a double.

The predefined operators expect all operands to be built-in types. Overloaded operators are only invoked if at least one of those operands is a user defined type. However, when user defined conversions to built-in types have been specified, the predefined operators may end up being used even when we use an operator with a user defined type for which an overloaded operator exists!

But wait, we are not done yet! We need to assign the result of our subtraction to our variable c. The type of the residual value is a double. The type of our variable c is complex. Therefore, that double must be converted into a complex before it can be assigned to the variable c. Our simple statement has required two user defined conversions, one to use the built-in subtraction operator and another to use the assignment operator.

Suppose we now use our original expression (c=c-d) after having provided the complex to double conversion function in our class. The following example illustrates what happens:

 

//main.cpp (Ex1310)

#include "complex.h"

 

int main() {

  complex c(1.0, 2.0);

  double  d(3.14159);

 

  c = c - d; //ambiguous overloaded function

  return (0);

}

 

It is now ambiguous which overloaded function to use. But wait a minute, we only have one overloaded function! How can it be ambiguous? The answer, of course, is that we must also consider the built-in operator functions. In this case, we have the following two operator function signatures to consider:

 

operator-(const complex &, const complex &) //member

operator-(double, double)                   //built-in

 

Neither signature is best. When comparing our use of the operator- function against our member function, we have an exact match with the first operand and the second operand requires a user defined conversion. But, when comparing against the built-in operator function, we have an exact match with the second operand and the first operand requires a user defined conversion. Since there is no operator function for which one argument is better and the remaining arguments are at least as good, the result is ambiguous.

This ambiguity only gets worse if we were to make the operator- function a non-member. Both expressions (c=c-d and c=d-c) would become ambiguous because of the complex to double user defined conversion function. Therefore, we recommend avoiding user defined conversion functions from class types to built-in types when overloading operators.

We have seen how implicit user defined conversions can help reduce the number of overloaded operator functions that we have to implement. We have also seen that the use of implicit user defined conversions can work behind the scenes to produce surprising results. If we include user defined conversions, be very careful to understand all of the contexts in which they will be used. If we can't ensure they will always produce the desired results, don't use them.

Practical Rule:  User defined type conversions cannot be applied to the first operand when operators are overloaded as members. Therefore, whenever we want to take advantage of the first operand being a different type, we must overload that operator as a non-member.

Practical Rule:  Minimize the use of implicit user defined conversions. Avoid implicit conversions from class types to built-in types, especially when used in conjunction with operator overloading.