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
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.
Go to CodeGuru.com
Contact: webmaster@codeguru.com
© Copyright 1997-1999 CodeGuru