MFC Programmer's SourceBook : Thinking in C++
Bruce Eckel's Thinking in C++, 2nd Ed Contents | Prev | Next

Automatic type conversion

In C and C++, if the compiler sees an expression or function call using a type that isn’t quite the one it needs, it can often perform an automatic type conversion from the type it has to the type it wants. In C++, you can achieve this same effect for user-defined types by defining automatic type-conversion functions. These functions come in two flavors: a particular type of constructor and an overloaded operator.

Constructor conversion

If you define a constructor that takes as its single argument an object (or reference) of another type, that constructor allows the compiler to perform an automatic type conversion. For example,

//: C12:Autocnst.cpp
// Type conversion constructor

class One {
public:
  One() {}
};

class Two {
public:
  Two(const One&) {}
};

void f(Two) {}

int main() {
  One one;
  f(one); // Wants a Two, has a One
} ///:~ 

When the compiler sees f( ) called with a One object, it looks at the declaration for f( ) and notices it wants a Two. Then it looks to see if there’s any way to get a Two from a One, and it finds the constructor Two::Two(One), which it quietly calls. The resulting Two object is handed to f( ).

In this case, automatic type conversion has saved you from the trouble of defining two overloaded versions of f( ). However, the cost is the hidden constructor call to Two, which may matter if you’re concerned about the efficiency of calls to f( ).

Preventing constructor conversion

There are times when automatic type conversion via the constructor can cause problems. To turn it off, you modify the constructor by prefacing with the keyword explicit[33] (which only works with constructors). Used to modify the constructor of class Two in the above example:

class One {
public:
  One() {}
};

class Two {
public:
  explicit Two(const One&) {}
};

void f(Two) {}

int main() {
  One one;
//!  f(one); // No auto conversion allowed
  f(Two(one)); // OK -- user performs conversion
}
By making Two’s constructor explicit, the compiler is told not to perform any automatic conversion using that particular constructor (other non- explicit constructors in that class can still perform automatic conversions). If the user wants to make the conversion happen, the code must be written out. In the above code, f(Two(one)) creates a temporary object of type Two from one, just like the compiler did in the previous version.

Operator conversion

The second way to effect automatic type conversion is through operator overloading. You can create a member function that takes the current type and converts it to the desired type using the operator keyword followed by the type you want to convert to. This form of operator overloading is unique because you don’t appear to specify a return type – the return type is the name of the operator you’re overloading. Here’s an example:

//: C12:Opconv.cpp
// Op overloading conversion

class Three {
  int i;
public:
  Three(int ii = 0, int = 0) : i(ii) {}
};

class Four {
  int x;
public:
  Four(int xx) : x(xx) {}
  operator Three() const { return Three(x); }
};

void g(Three) {}

int main() {
  Four four(1);
  g(four);
  g(1);  // Calls Three(1,0)
} ///:~ 

With the constructor technique, the destination class is performing the conversion, but with operators, the source class performs the conversion. The value of the constructor technique is you can add a new conversion path to an existing system as you’re creating a new class. However, creating a single-argument constructor always defines an automatic type conversion (even if it’s got more than one argument, if the rest of the arguments are defaulted), which may not be what you want. In addition, there’s no way to use a constructor conversion from a user-defined type to a built-in type; this is possible only with operator overloading.

Reflexivity

One of the most convenient reasons to use global overloaded operators rather than member operators is that in the global versions, automatic type conversion may be applied to either operand, whereas with member objects, the left-hand operand must already be the proper type. If you want both operands to be converted, the global versions can save a lot of coding. Here’s a small example:

//: C12:Reflex.cpp
// Reflexivity in overloading

class Number {
  int i;
public:
  Number(int ii = 0) : i(ii) {}
  const Number
  operator+(const Number& n) const {
    return Number(i + n.i);
  }
  friend const Number
    operator-(const Number&, const Number&);
};

const Number
  operator-(const Number& n1,
            const Number& n2) {
    return Number(n1.i - n2.i);
}

int main() {
  Number a(47), b(11);
  a + b; // OK
  a + 1; // 2nd arg converted to Number
//! 1 + a; // Wrong! 1st arg not of type Number
  a - b; // OK
  a - 1; // 2nd arg converted to Number
  1 - a; // 1st arg converted to Number
} ///:~ 

Class Number has a member operator+ and a friend operator–. Because there’s a constructor that takes a single int argument, an int can be automatically converted to a Number, but only under the right conditions. In main( ), you can see that adding a Number to another Number works fine because it’s an exact match to the overloaded operator. Also, when the compiler sees a Number followed by a + and an int, it can match to the member function Number::operator+ and convert the int argument to a Number using the constructor. But when it sees an int and a + and a Number, it doesn’t know what to do because all it has is Number::operator+, which requires that the left operand already be a Number object. Thus the compiler issues an error.

With the friend operator–, things are different. The compiler needs to fill in both its arguments however it can; it isn’t restricted to having a Number as the left-hand argument. Thus, if it sees 1 – a , it can convert the first argument to a Number using the constructor.

Sometimes you want to be able to restrict the use of your operators by making them members. For example, when multiplying a matrix by a vector, the vector must go on the right. But if you want your operators to be able to convert either argument, make the operator a friend function.

Fortunately, the compiler will not take 1 – 1 and convert both arguments to Number objects and then call operator–. That would mean that existing C code might suddenly start to work differently. The compiler matches the “simplest” possibility first, which is the built-in operator for the expression 1 – 1 .

A perfect example: strings

An example where automatic type conversion is extremely helpful occurs with a string class. Without automatic type conversion, if you wanted to use all the existing string functions from the Standard C library, you’d have to create a member function for each one, like this:

//: C12:Strings1.cpp
// No auto type conversion
#include <cstring>
#include <cstdlib>
#include "../require.h"
using namespace std;

class Stringc {
  char* s;
public:
  Stringc(const char* S = "") {
    s = (char*)malloc(strlen(S) + 1);
    require(s != 0);
    strcpy(s, S);
  }
  ~Stringc() { free(s); }
  int Strcmp(const Stringc& S) const {
    return ::strcmp(s, S.s);
  }
  // ... etc., for every function in string.h
};

int main() {
  Stringc s1("hello"), s2("there");
  s1.Strcmp(s2);
} ///:~ 

Here, only the strcmp( ) function is created, but you’d have to create a corresponding function for every one in <cstring> that might be needed. Fortunately, you can provide an automatic type conversion allowing access to all the functions in <cstring>:

//: C12:Strings2.cpp
// With auto type conversion
#include <cstring>
#include <cstdlib>
#include "../require.h"
using namespace std;

class Stringc {
  char* s;
public:
  Stringc(const char* S = "") {
    s = (char*)malloc(strlen(S) + 1);
    require(s != 0);
    strcpy(s, S);
  }
  ~Stringc() { free(s); }
  operator const char*() const { return s; }
};

int main() {
  Stringc s1("hello"), s2("there");
  strcmp(s1, s2); // Standard C function
  strspn(s1, s2); // Any string function!
} ///:~ 

Now any function that takes a char* argument can also take a Stringc argument because the compiler knows how to make a char* from a Stringc.

Pitfalls in automatic type conversion

Because the compiler must choose how to quietly perform a type conversion, it can get into trouble if you don’t design your conversions correctly. A simple and obvious situation occurs with a class X that can convert itself to an object of class Y with an operator Y( ) . If class Y has a constructor that takes a single argument of type X, this represents the identical type conversion. The compiler now has two ways to go from X to Y, so it will generate an ambiguity error when that conversion occurs:

//: C12:Ambig.cpp
// Ambiguity in type conversion

class Y; // Class declaration

class X {
public:
  operator Y() const; // Convert X to Y
};

class Y {
public:
  Y(X); // Convert X to Y
};

void f(Y);

int main() {
  X x;
//! f(x); // Error: ambiguous conversion
} ///:~ 

The obvious solution to this problem is not to do it: Just provide a single path for automatic conversion from one type to another.

A more difficult problem to spot occurs when you provide automatic conversion to more than one type. This is sometimes called fan-out:

//: C12:Fanout.cpp
// Type conversion fanout

class A {};
class B {};

class C {
public:
  operator A() const;
  operator B() const;
};

// Overloaded h():
void h(A);
void h(B);

int main() {
  C c;
//! h(c); // Error: C -> A or C -> B ???
} ///:~ 

Class C has automatic conversions to both A and B. The insidious thing about this is that there’s no problem until someone innocently comes along and creates two overloaded versions of h( ). (With only one version, the code in main( ) works fine.)

Again, the solution – and the general watchword with automatic type conversion – is to only provide a single automatic conversion from one type to another. You can have conversions to other types; they just shouldn’t be automatic. You can create explicit function calls with names like make_A( ) and make_B( ).

Hidden activities

Automatic type conversion can introduce more underlying activities than you may expect. As a little brain teaser, look at this modification of FeeFi.cpp:

//: C12:FeeFi2.cpp
// Copying vs. initialization

class Fi {};

class Fee {
public:
  Fee(int) {}
  Fee(const Fi&) {}
};

class Fo {
  int i;
public:
  Fo(int x = 0) { i = x; }
  operator Fee() const { return Fee(i); }
};

int main() {
  Fo fo;
  Fee fiddle = fo;
} ///:~ 

There is no constructor to create the Fee fiddle from a Fo object. However, Fo has an automatic type conversion to a Fee. There’s no copy-constructor to create a Fee from a Fee, but this is one of the special functions the compiler can create for you. (The default constructor, copy-constructor, operator=, and destructor can be created automatically.) So for the relatively innocuous statement

Fee fiddle = FO;

the automatic type conversion operator is called, and a copy-constructor is created.

Automatic type conversion should be used carefully. It’s excellent when it significantly reduces a coding task, but it’s usually not worth using gratuitously.


[33] At the time of this writing, explicit was a new keyword in the language. Your compiler may not support it yet.

Contents | Prev | Next


Go to CodeGuru.com
Contact: webmaster@codeguru.com
© Copyright 1997-1999 CodeGuru