Previous section   Next section

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


16.4. explicit(_cast)

The perils of implicit conversion operators have been very well documented in recent years ([Sutt2000, Meye1996, Dewh2003, Stro1997]), and can lead to all kinds of unintended consequences—both semantic and performance—in the manipulation of types. Item 36 from [Dewh2003] and Item 5 from [Meye1996] illustrate some of the problems.

Consider the gauche class shown in Listing 16.4, which provides a number of implicit conversion operators. Very few people would write a class such as this. But in some respects it is reasonable, since date/time manipulations do come in a variety of types, and a useful time class will be compatible with all such types as are needed.

Listing 16.4.



class Time


{


  operator std::time_t () const;


  operator std::tm () const;


#if defined(UNIX)


  operator struct timeval () const;


#elif defined(WIN32)


  operator DATE () const;


  operator FILETIME () const;


  operator SYSTEMTIME () const;


#endif /* operating system */


};



But we're imperfect practitioners! Tenet #2 (see Prologue) states that we wear a hairshirt, so we certainly don't want implicit conversions. But tenet #4 states that we do not accept the limitations placed upon us, so let's seek out a solution.

explicit is already a keyword in C++, having the function of preventing the constructor to which it is applied from taking part in an implicit construction of the given class, known as conversion construction (see section 2.2.7). We'll look here at another potentially very useful application of the keyword, which is sadly missing from the language. Consider the rewritten pseudo-C++ form in Listing 16.5.

Listing 16.5.


class Time


{


  explicit operator std::time_t () const;


  explicit operator std::tm () const;


#if defined(UNIX)


  explicit operator struct timeval () const;


#elif defined(WIN32)


  explicit operator DATE () const;


  explicit operator FILETIME () const;


  explicit operator SYSTEMTIME () const;


#endif /* operating system */


};



The explicit keyword marks the two conversion operators as being invalid for consideration for implicit conversion. What this means is that instances of Time would not be implicitly convertible to time_t, or struct timeval or any of the other time types, but they would be amenable to explicit conversions, as follows:



Time       t  = . . .;


std::time_t v1 = t;                // error – need explicit conv


#if defined(WIN32)


FILETIME    v2 = static_cast<FILETIME>(t);  // Explicit conv Ok!


#endif /* operating system */



The big win would be that multiple valid conversion operators could be supported, where currently one must eschew some or all of them due to ambiguities. Since the language does not provide this use of the explicit keyword, there are three solutions available to us.

16.4.1 Use Explicit Accessor Methods

The standard recommended approach [Meye1996] is to accept more typing and use normal method calls instead of the implicit conversion operators:



class Time


{


  . . .


  std::time_t get_time_t() const;


  std::tm     get_tm() const;


  . . .


  SYSTEMTIME  get_SYSTEMTIME() const;


#endif /* operating system */


};



We can give in to more typing, and simply write:



Time       t  = . . .;


std::time_t v1 = t.get_time_t(); // Explicit method call: clear



The benefit of this approach is that, given meaningful accessor method names, it is clear what is being returned. This is the approach advocated by the standard library for its string type, as in:



std::string s("A string object);





puts(s);            // Error – no suitable conversion


puts(s.c_str());    // Return a c-style string



The main drawback of this approach is that it is highly fragile with respect to generality. It relies on all types that return a given value type using precisely the same name. If that works, then it is feasible to write generalized code—template or otherwise—that may be used with a variety of such types. But we all know how easy it is to get it wrong and have that mistake cemented by inertia. And that's before we consider differences of opinion over naming conventions: Is that get_time_t() or get_timet() or get_std_time_t()...?

Another drawback from my perspective is that the semantics (to a human, at least) are misleading. If I call a function get_XYZ() on something, I might resonably expect to be getting the XYZ that it owns, rather than receiving it as an XYZ. Granted that the difference is subtle, but subtlety is the eager precursor to confusion in this crazy game we play. Of course, one could call it as_XYZ(), but conventions dictate that get_ is the prefix for such methods, and flouting naming conventions is never a win-win game.

16.4.2 Emulate Explicit Casts

Since a compiler is only allowed to provide at most one implicit conversion in an expression, we can provide implicit conversion operators to an intermediate type, which is itself convertible to the actual result type. For example:

Listing 16.6.


struct tm_cast


{


  tm_cast(std::tm v)


    : m_v(v)


  {}


  operator std::tm() const


  {


    return m_v;


  }


  std::tm m_v;


};





class Time


{


  . . .


  operator tm_cast() const;


  . . .


};





Time   t  = . . .;


std::tm v1 = static_cast<tm_cast>(t); // Conversion via tm_cast



Now we have three types involved in each of our conversions, for example, Time => tm_cast => std::tm.. To get the compiler to work out the second step in each conversion we need to give it the middle step and it can take it from there. Naturally, it would be much nicer if we could supply the third type, the one that is needed by the functions f1 and f2, and have the compiler provide the first conversion implicitly:



f1(static_cast<std::tm>(t));   // Illegal, but clearer to reader



Understandably, the compiler cannot have this level of intuition, as it would be too easy to tie it up in knots: What if both the intermediate types had the same implicit conversion type? The syntax does look very appealing though, doesn't it?

What if there was another way to achieve this? How about the following:



class Time


{


  . . .


  operator explicit_cast<std::tm>() const;


  . . .


};





Time   t  = . . .;


std::tm v1 = explicit_cast<std::tm >(t); // correct & clear



It is relatively straightforward to create the explicit_cast template, and we look in detail at this in section 19.5. Note its more obvious syntax: it specifies precisely the type that we are explicitly casting to, rather than the intermediate type. It's pretty much self-documenting!

The other significant plus is that it is a general solution. We have one template, rather than the potentially limitless tm_cast, DATE_cast, std_string_ref_const_cast, and so forth intermediate types. In template code, a parameterizing type can be applied to the explicit_cast parameter, supporting type generality with aplomb.

As we see in section 19.5, this works really well for fundamental types, and for pointer types, but does not work well for const references and not at all for non-const references. As such, it is only a partial solution, for all its conceptual appeal.

16.4.3 Use Attribute Shims

The last alternative is to combine the explicit accessor method approach (see section 16.4.1) with attribute shims. A shim is a collection of one or more free function overloads that is used to elicit common attributes from instances of unrelated types; shims are a powerful generalizing mechanism. We learn all about them in Chapter 20, so I'll defer a detailed explanation of them until then. For the moment, let's just look at the two shims and the resultant client code:



// get_tm() attribute shim - uses Time::get_tm()


inline std::tm get_tm(Time const &t)


{


  return t.get_tm();


}


// get_time_t() attribute shim - uses Time::get_time_t()


inline std::time_t get_time_t(Time const &t)


{


  return t.get_time_t();


}





Time       t  = . . .;


std::tm     v1 = get_tm(t);     // Calls get_tm() attribute shim


std::time_t v2 = get_time_t(t); // Calls get_time_t() attribute shim


f1(get_tm(t));    // Calls get_tm() attribute shim


f2(get_DATE(t));  // Calls get_DATE()attribute shim



This is the best general solution, and works correctly with all types and flavors (reference, pointer, const and/or volatile) thereof, without the drawbacks of the other approaches. As we see in Chapter 20 and later in the book, the use of shims is a great generalizing mechanism, albeit that it is somewhat verbose. (It also suffers the get_ vs. as_ naming, but no one's perfect!)


      Previous section   Next section