[ Team LiB ] Previous Section Next Section

Gotcha #37: Unintended Constructor Conversion

A single-argument constructor specifies both an initialization and a conversion. As with a conversion operator, a constructor conversion may be applied implicitly by the compiler. This is sometimes convenient:



class String { 


 public:


   String( const char * );


   operator const char *() const;


   // . . .


};


String name1( "Fred" ); // direct init


name1 = "Joe"; // implicit conversion


const char *cname = name1; // implicit conversion


String name2 = cname; // implicit conversion, copy init


String name3 = String( cname ); // explicit conversion, copy init


(See Gotcha #56.) However, implicit constructor conversions can often render code hard to understand and can introduce obscure bugs. Consider a container template for a stack of fixed, maximum size:



template <class T> 


class BoundedStack {


 public:


   BoundedStack( int maxSize );


   ~BoundedStack();


   bool operator ==( const BoundedStack & ) const;


   void push( const T & );


   void pop();


   const T &top() const;


   // . . .


};


Our BoundedStack types have the usual stack operations of push, pop, and so on, as well as the ability to compare two stacks for equality. When we create a BoundedStack<T>, we must provide its maximum size.



BoundedStack<double> s( 128 ); 


s.push( 37.0 );


s.push( 232.78 );


// . . .


Unfortunately, the single-argument constructor may be applied as a conversion in cases where we would probably have preferred a compile-time error:



if( s == 37 ) { // oops! 


   // . . .


In this case, chances are that we intended to have a condition like s.top() == 37. Unfortunately, the condition will compile without error, because the compiler is able to convert the integer value 37 into a BoundedStack<double> and pass it as an argument to BoundedStack<double>::operator ==. Effectively, the compiler generates the following code:



BoundedStack<double> stackTemp( 37 ); 


bool resultTemp( s.operator ==( stackTemp ) );


stackTemp.~BoundedStack<double>();


if( resultTemp ) {


   // . . .


The resulting code is legal, incorrect, and expensive. A safer alternative would be to declare the BoundedStack constructor to be explicit. The explicit keyword tells the compiler that it may not use the constructor as an implicit conversion, although it may still be used as an explicit conversion:



template <class T> 


class BoundedStack {


 public:


   explicit BoundedStack( int maxSize );


   // . . .


};


// . . .


if( s == 37 ) { // error, fortunately


   // . . .


if( s.top() == 37 ) { // correct, no conversion


   // . . .


if( s == static_cast< BoundedStack<double> >(37) ) { // correct . . .


   // . . .


Insidious implicit conversions are much more to be feared than the occasional necessity of performing an explicit conversion, so it's common and recommended to declare most single-argument constructors explicit.

Note that declaring a constructor explicit also affects the set of legal initialization syntaxes one may employ when declaring an object of a class. Let's make a change to our String class above and see how it affects the set of legal initializations:



class String { 


 public:


   explicit String( const char * );


   operator const char *() const;


   // . . .


};


String name1( "Fred" ); // OK.


name1 = "Joe"; // error!


const char *cname = name1; // implicit conversion, OK


String name2 = cname; // error!


String name3 = String( cname ); // explicit conversion, OK


The implicit temporary generation that is part of the copy initializations of name2 and the argument of String::operator = are now illegal. The initialization of name3 is still legal, because the conversion is explicit (although it would have been better form to perform the initialization with a static_cast; see Gotcha #40). As usual, it's best to use direct initialization in preference to copy initialization. (See Gotcha #56.)

Before we leave the subject of explicit behind, let's have a look at an instructive, but now outmoded, technique for implementing the semantics of explicit without use of the keyword:



class StackInit { 


 public:


   StackInit( size_t s ) : size_( s ) {}


   int getSize() const { return size_; }


 private:


   int size_;


};


template <class T>


class BoundedStack {


 public:


   BoundedStack( const StackInit &init );


   // . . .


};


Because the BoundedStack constructor isn't declared to be explicit, the compiler will attempt to convert implicitly any StackInit objects to BoundedStack. However, the compiler won't make the attempt to convert an integer to a StackInit and follow that implicit conversion with a second implicit conversion of StackInit to BoundedStack. The standard specifies that the compiler will attempt only a single implicit user-defined conversion at a time:



BoundedStack<double> s( 128 ); // OK. 


BoundedStack<double> t = 128; // OK.


if( s == 37 ) { // error!


   // . . .


This technique gives us behavior almost identical to that of explicit. The declaration of s and t is legal, because only a single user-defined conversion is necessary to convert 128 into a StackInit to pass to the constructor. However, the compiler will not attempt to convert 37 to a BoundedStack<double>, because that would require a sequence of two user-defined conversions: int to StackInit and StackInit to BoundedStack<double>.

    [ Team LiB ] Previous Section Next Section