devxlogo

Restrain Conversion Operators with the “Indirect Conversion” Idiom

Restrain Conversion Operators with the “Indirect Conversion” Idiom

onversion operators are a dilemma. While they certainly make the usage of certain objects convenient and intuitive, they also compromise type safety, ultimately leading to unexpected runtime behavior. The following sections present a technique that significantly reduces the potential hazards of implicit conversions to bool. This technique is now becoming a de-facto standard in C++ libraries, like Boost.


You want to add a conversion operator to your class, but you’re concerned about the perils of implicit conversions.


Use the “indirect conversion” idiom to thwart undesirable conversions while still permitting well-behaved conversions.

Presenting the Problem
Suppose you’re designing a smart pointer class. In addition to overloading the operators * and ->, a smart pointer class usually defines a conversion operator to bool:

template class Ptr{public: operator bool() const {  return (rawptr ? true: false); }//..more stuffprivate: T * rawptr;};

The conversion to bool enables clients to use smart pointers in expressions that require bool operands:

Ptr ptr(new int);if(ptr ) //calls operator bool() cout

Furthermore, the implicit conversion to bool is required in conditional declarations such as:

if (shared_ptr px = dynamic_pointer_cast(py)){ //we get here only of px isn't empty} 

Alas, this automatic conversion opens the gate to unwelcome surprises:

Ptr  p1;Ptr  p2;//surprise #1cout pf;Ptr  pq; // Query and File are unrelated //surprise #2if(pf==pq) //compares bool values, not pointers! 

booling Around
In the pre-bool era, C++ libraries often used void* as the target type of a Boolean conversion operator. The library is an example of this. Will void* save the day? Indeed, a void* conversion operator prevents nonsensical expressions such as p1+p2 from compiling:

template class Ptr{public: operator void*() const {  return ((void*) rawptr); }};cout pf;Ptr  pq; // Query and File are unrelated typesif(pf==pq) // so so, compares raw addresses 

And yet, a void* conversion operator introduces new surprises:

cout 

The Pointer to Member Hack
Recently, C++ pundits have come up with a better idea. Instead of using bool or void*, classes such as std::tr1::shared_ptr use pointers to members as the target type of a Boolean conversion operator.

Author's Note: The terms "conversion to bool" and "conversion to Boolean" aren't interchangeable. In the former, bool is the target type. The latter however uses an unspecified type that's convertible to bool.

Let's look at a concrete implementation of this technique.

First, add a nested struct that has one data member to class Ptr (code changes are highlighted):

template class Ptr{private: struct PtrConversion {  int valid; };};

Next, declare a typedef that serves as a synonym for "pointer to an int member of PtrConversion":

template class Ptr{private: struct PtrConversion {  int valid; };public: typedef int PtrConversion::*pmi;};

Finally, define a public conversion operator whose target type is pmi:

public:operator pmi() const //conversion to Boolean { return rawptr? &PtrConversion::valid : 0;}

Smooth Operator
Let's analyze the return statement. If rawptr isn't NULL, the conversion operator returns a pointer to a member of struct PtrConversion (the precise value of the expression &PtrConversion::valid is immaterial; what matters is that it's a non-NULL constant). However, if rawptr is NULL, the conversion operator returns a NULL pmi. Recall that all pointers to members are implicitly convertible to bool; they aren't bool variables, though. This subtle difference is crucial, as you will see.

After this change, when the following statement executes:

if(ptr)//...

C++ calls ptr.operator pmi() behind the scenes. The return value of type pmi is then silently converted to bool, as expected. However, the following statement no longer compiles, which is exactly what you want:

cout

Here's the secret: C++ guarantees that an operand shall undergo at most one implicit conversion. In the if(ptr) example, you have exactly one implicit conversion from pmi to bool (the very invocation of a conversion operator doesn't constitute an implicit conversion for this matter). However, in the expression p1+p2 two implicit conversions per operand are necessary:

  1. pmi?>bool
  2. bool?>int

As I said, C++ doesn't allow this.

Let's see how this technique eliminates the problems caused by a void* conversion operator:

cout 

This statement no longer compiles since there is no overloaded version of operator pmi or Ptr. Similarly, the following delete expression:

delete pf; //compilation error

doesn't compile because neither pmi nor bool are valid types in a delete expression.

Twisting by the bool
The boost::shared_ptr class uses a slightly different version of the indirect conversion technique. Instead of using a dummy nested struct, it uses a pointer to a member of class shared_ptr itself. In terms of memory footprint, there is no difference between the two versions because the dummy struct is not instantiated anyway. However, some compilers have difficulties coping with pointers to member functions of a class template so the Boost library uses conditional compilation to switch between a pointer to a data member and a pointer to a member function as the target type of the Boolean conversion operator. The bottom line is this: instead of bool, void* or int as the target type of a Boolean conversion operator, use a pointer to member to minimize the perils of implicit conversions.

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist