Previous section   Next section

Imperfect C++ Practical Solutions for Real-Life Programming
By Matthew Wilson
Table of Contents
Chapter 19.  Casts


19.5. explicit_cast

Now we know a lot more about casts, we can look closely at the explicit_cast that was mentioned in section 16.4. The problem was how to provide explicit casts—to std::tm and DATE—for our Time class while avoiding the problems of implicit conversions. The second option suggested was to use an explicit_cast component.



class Time


{


  operator explicit_cast<tm> () const;


  operator explicit_cast<DATE> () const;


};



Given what we've just learned about implementing cast operators as template classes, we can postulate an implementation of explicit_cast such as the following:

Listing 19.5.


template <typename T>


class explicit_cast


{


// Construction


public:


  explicit_cast(T t)


      : m_t(t)


  {}


// Conversions


public:


  operator T () const


  {


    return m_t;


  }


// Members


private:


  T   m_t;


};



This works really well for fundamental and pointer types. Alas, this being C++, such a simple thing could never be the final solution. The problem is that m_t is a member variable, and gets copied from t in the cast constructor. When T is a fundamental type, this copying is usually fine. In fact, compilers easily optimize the whole shebang out of existence for fundamental and pointer types[4] (and references, subject to the limitations described below).

[4] Of course, if you want to pass out a long double value (which is either 64 or 80 bits on most 32-bit compilers [Kaha1998]) on a 32-bit architecture, then you may opt to express it as the slightly more efficient form of explicit_cast<long double const &> rather than explicit_cast<long double>, since your client code may further pass on the value as a reference (to const); hence you avoid an extra copy. However, this has nothing directly to do with the efficiency of explicit_cast in this case, merely what your choice of cast type may affect elsewhere in your code.

It seems to be pretty complete for fundamental types. How will it work for user-defined types? Well, if T is a type with a nontrivial cost to construction, such as a vector of strings, then you will pay a potentially unacceptable cost of copying.



class Path


{


public:


  operator explicit_cast<string> () const;


};


void ProcessPath(Path const &path)


{


  string  s = explicit_cast<string>(path); // multiple copies!


  . . .



In fact, in code like that above, there may be several copies in a single cast operation. Borland, Digital Mars, GCC, Intel, and Watcom all create three copies; CodeWarrior and Visual C++ (without Microsoft extensions) create four; Visual C++ (with Microsoft extensions) creates 5! (And that was when compiled with maximum speed optimizations.) Needless to say, this is something to be avoided. However, since all programmers should know that copying nontrivial types involves costs, we can arguably accept this as an acknowledged artefact of such return-by-value.

Another problem arises when using references. This is because the language does not allow a non-const reference to a temporary. This is an eminently sensible rule in general because, if it did so, a piece of code would be able to change a temporary, which could have the confused programmer wondering what happened to their change as it disappeared along with the temporary instance, never to be seen again. Item #30 in Effective C++ [Meye1998] discusses this, and also explains very well why one should avoid returning non-const references to members from class methods. That goes double for being able to cast to non-const references, so I would say that even though the reference-to-non-const temporary would be valid in this case—since the temporary merely passes along a reference that it "validly" obtained—we should not be troubled where such uses do not compile.



class Path


{


public:


  operator explicit_cast<string &> (); // Danger!


};


void ProcessPath(Path &path)


{


  string &s = explicit_cast<string&>(path); // Not legal or nice


  . . .



So that just leaves us with casting to reference-to-const. There is no reason why our cast should not work; merely that it does not with some compilers. Actually the cast compiles, but the problem is that some compilers create intermediate temporary copies of the type of which a reference is being returned, and do so without issuing any warnings. Specifically, Intel 6.0, 7.0 and 7.1 and Visual C++ 6.0 and 7.0 each seem to create an extra temporary. Borland, Digital Mars, CodeWarrior, GCC, Visual C++ 7.1, and Watcom all behave as desired.



class Path


{


public:


  operator explicit_cast<string const &>() const;


};


void ProcessPath(Path const &path)


{


  // Works, but a temporary may be created here


  string const &s = explicit_cast<string const &>(path);


  . . .



To summarize: we have a missing language feature—explicit casts—that, though needed only rarely, is nonetheless useful. (We'll see some examples of such use in Part Six.) We have a component that provides ideal behavior with fundamental types and all types of pointers, but has problems with references. It does not work for references-to-non-const, but we're content with that because even if it did not violate the language rules, it would be an unwise and rarely useful capability. The component is flawed, however, insofar as it can create unwanted copies in some compilers and not others. Such inconsistencies in the behavior of code between compilers, even if the "fault" lies with some of the compilers themselves,[5] is not acceptable in quality software, so we need to do something about this. It's simply not acceptable to write a generic component and say that it can and should be used for some compilers, but not for others, given that it will compile without warning on the ones where it does not work as expected. People will rightly refuse to use it, and will probably be turned off all your other work as well.

[5] Strictly, this is an optimization issue, rather than one of correctness, and pertains to a particular implementation's interpretation of the standard insofar as it elects to elide copy constructors. (See Chapter 18.)

What's to be done? Well, we need to prevent explicit_cast being used for references to user-defined types, while allowing it to be used for references to (const) fundamental types. There are two ways with which we can forcibly proscribe inappropriate use of this component: specialization and constraints.

The first way, using both partial and full specialization, looks like the following:

Listing 19.6.


// Prohibit all reference types


template <typename T>


class explicit_cast<T &>


{


// Construction


private:


  explicit_cast(T &);


// Conversions


private:


  operator T & ();


};





// Explicitly allow specific (fundamental) types





template <>


class explicit_cast<char const &>


{


// Construction


public:


  explicit_cast(char const &t)


    : m_t(t)


  {}


// Conversions


public:


  operator char const & () const


  {


    return m_t;


  }


// Members


private:


  char const &m_t;


};





. . . // repeat for bool, wchar_t


. . . // repeat for signed & unsigned char


. . . // repeat for (unsigned) short/int/long/long long


. . . // repeat for float, double & long double





// Enable for all pointer types


template <typename T>


class explicit_cast<T *>


{


// Construction


public:


  explicit_cast(T *t)


    : m_t(t)


  {}


// Conversions


public:


  operator T * ()


  {


    return m_t;


  }


// Members


private:


  T   *m_t;


};



The first partial specialization explicit_cast<T&> declares its constructor and implicit conversion operator private, which essentially prevents any references being used in explicit casts. This is good, but too powerful, since we want the fundamental types to work for reference-to-const casts. That's where the full specializations come in. The one shown is for char, and there are corresponding specializations for all the fundamental types.[6] The picture is completed by the provision of a partial specialization for pointer types, to enable them to be used, as some compilers may get confused between the template itself and the reference specialization when asked to instantiate for pointer types. (If in doubt, be explicit, if you'll pardon the pun.) With these specializations we have the precise behavior we want.

[6] On (nonconformant) compilers that do not support wchar_t as a distinct type, you'd have to have the appropriate preprocessor discrimination to hide that definition, since it is not legal to provide two specializations of the same type, even where the definitions are identical.

(Note that if you want the extra caution of prohibiting any pointers to non-const being returned, you can change the pointer specialization to use private access control in the same way as the reference one did, and then further specialize a pointer to const one, that is, explicit_cast<T const *> with public constructor and conversion operator. It's up to you.)

Alas, not all compilers in common use support partial specialization, so it's prudent to provide a correspondingly restricted version of explicit_cast in such circumstances. This relies on constraints. We apply the constraint_must_be_pod() constraint (section 1.2.4) in the destructor, which means that the template cannot be used for non-POD types.

Listing 19.7.


template <typename T>


class explicit_cast


{


  . . .


#ifndef ACMELIB_TEMPLATE_PARTIAL_SPECIALIZATION_SUPPORT


  // For compilers that don't support partial specialization we


  // enforce the constraint that only POD types may be used.


  ~explicit_cast()


  {


    constraint_must_be_pod(T);


  }


#endif /* ! ACMELIB_TEMPLATE_PARTIAL_SPECIALIZATION_SUPPORT */


  . . .


};



That's it! It's not perfect, because it does allow the template to be applied to nontrivial POD types, for example, large structures. However, since we've already decided that the cost of copying with by-value parameterization of the template is a matter of caveat emptor, in the rare cases where a compiler cannot optimize intermediate copies of the structure, it's your responsibility for electing to pass by value. What the constraint does buy us though—and remember that this is at compile time—is the rejection of any nontrivial user-defined types with potentially costly copying.

One great thing about explicit_cast is that it can be applied, in client code, both to types that express explicit_cast operators and to those that provide implicit conversion operators. This means that you can write generic code to both kinds of types—the cautiously written and the injudiciously written—in the same way explicit_cast can, therefore be the basis of a generic conversion mechanism, where the exercising of the choice of whether, and to what, to convert is in the hands of the programmer, rather than the compiler. That's where it should be.


      Previous section   Next section