PxxxxR0
down_cast

New Proposal,

This version:
http://virjacode.com/papers/down_cast000.htm
Latest version:
http://virjacode.com/papers/down_cast.htm
Author:
Thomas PK Healy <healytpk@vir7ja7code7.com> (Remove all sevens from email address)
Audience:
SG18
Project:
ISO/IEC 14882 Programming Languages — C++, ISO/IEC JTC1/SC22/WG21

Abstract

Add a new function to the standard library called std::down_cast to be used as a more-restrictive form of dynamic_cast. Normally dynamic_cast can perform an up-cast, a down-cast or a side-cast (a.k.a cross-cast), however the proposed new function std::down_cast can only perform a down-cast.

1. Introduction

Given the following inheritence diagram which you might find in a video game:

          Vehicle
        /         \
       V           V
Aircraft           Watercraft
        \         /
         V       V
          SeaPlane

This would be written in C++ as:

class Vehicle { virtual ~Vehicle(void) = default; };
class Aircraft   : virtual public Vehicle {};
class Watercraft : virtual public Vehicle {};
class SeaPlane   : public Aircraft, public Watercraft {};

If we create an object of type SeaPlane, and then cast it to a reference to its base class Aircraft, we can then use dynamic_cast to convert the Aircraft& to a Watercraft&, and this is called a cross-cast or a side-cast, for example:

SeaPlane var;
Aircraft   &v1 = var;
Watercraft &v2 = dynamic_cast<WaterCraft&>(v1);

If we create an object of type SeaPlane, and then cast it to a reference to its base class Vehicle, this is called an up-cast. An up-cast will always succeed, and therefore it makes more sense to use static_cast instead of dynamic_cast for this purpose, or even just to allow an implicit conversion to take place:

SeaPlane var;
Vehicle &v1 = static_cast<Vehicle&>(var);
Vehicle &v2 = var;

The one remaining kind of cast in a class inheritance hierarchy is the down-cast. This is where we would have a reference or a pointer to a Aircraft, and we want to check if we have a SeaPlane, for instance:

void LogSeaPlane(Aircraft const &arg)
{
    if ( dynamic_cast<SeaPlane const*>(&arg) ) Logger::Write("We have a seaplane!");
}

This paper proposes a new more-restrictive form of dynamic_cast which can only perform a down-cast.

1.1. Usage

class Vehicle { public: virtual ~Vehicle(void) = default; };
class Aircraft   : virtual public Vehicle {};
class Watercraft : virtual public Vehicle {};
class SeaPlane   : public Aircraft, public Watercraft {};

auto main(void) -> int
{
    Vehicle var;

    Watercraft &water = std::down_cast<Watercraft&>(var);

    Aircraft &a =   dynamic_cast<Aircraft&>( water);  // succeeds
    Aircraft &b = std::down_cast<Aircraft&>( water);  // throws std::bad_cast
    Aircraft *c = std::down_cast<Aircraft*>(&water);  // return nullptr
}

2. Possible Implementation

2.1. GNU g++

#include <cassert>      // assert
#include <concepts>     // derived_from
#include <typeinfo>     // typeid, type_info
#include <type_traits>  // is_const, is_pointer, is_polymorphic, is_volatile, remove_cvref, remove_pointer
#include <tinfo.h>      // __class_type_info, vtable_prefix

template<typename DstPtr, typename SrcPtr>
requires (std::is_pointer_v<DstPtr> && std::is_pointer_v<SrcPtr>)
DstPtr down_cast(SrcPtr const src_ptr)
{
  using Dst = std::remove_pointer_t<DstPtr>;  // might be const volatile
  using Src = std::remove_pointer_t<SrcPtr>;  // might be const volatile

  if ( false == std::is_polymorphic_v<Dst> ) return nullptr;  // Destination type must be polymorphic
  if ( false == std::derived_from<Dst,Src> ) return nullptr;  // Source type must be a public base class of Destination type

  static_assert( std::is_const_v   <Dst> || !std::is_const_v<Src>   );
  static_assert( std::is_volatile_v<Dst> || !std::is_volatile_v<Src>);

  using namespace __cxxabiv1;

  __class_type_info const *const dst_type = dynamic_cast<__class_type_info const *>(&typeid(std::remove_cvref_t<Dst>));
  __class_type_info const *const src_type = dynamic_cast<__class_type_info const *>(&typeid(*src_ptr));
  assert( nullptr != dst_type );
  assert( nullptr != src_type );

  if (__builtin_expect(!src_ptr, 0))
    return NULL; // Handle precondition violations gracefully.

  const void *vtable = *static_cast <const void *const *>(static_cast<void*>(src_ptr));
  const vtable_prefix *prefix =
    (adjust_pointer <vtable_prefix>
     (vtable,  -ptrdiff_t (offsetof (vtable_prefix, origin))));
  const void *whole_ptr =
      adjust_pointer <void> (src_ptr, prefix->whole_object);
  const __class_type_info *whole_type = prefix->whole_type;
  __class_type_info::__dyncast_result result;

  // If the whole object vptr doesn't refer to the whole object type, we're
  // in the middle of constructing a primary base, and src is a separate
  // base.  This has undefined behavior and we can't find anything outside
  // of the base we're actually constructing, so fail now rather than
  // segfault later trying to use a vbase offset that doesn't exist.
  const void *whole_vtable = *static_cast <const void *const *> (whole_ptr);
  const vtable_prefix *whole_prefix =
    (adjust_pointer <vtable_prefix>
     (whole_vtable, -ptrdiff_t (offsetof (vtable_prefix, origin))));
  if (whole_prefix->whole_type != whole_type)
    return NULL;

  whole_type->__do_dyncast (-1, __class_type_info::__contained_public,
                            dst_type, whole_ptr, src_type, src_ptr, result);
  if (!result.dst_ptr)
    return NULL;
  if (contained_public_p (result.dst2src))
    // Src is known to be a public base of dst.
    return static_cast<DstPtr>(const_cast <void *> (result.dst_ptr));
  if (contained_public_p (__class_type_info::__sub_kind
              (result.whole2src & result.whole2dst)))
    // Both src and dst are known to be public bases of whole. Found a valid
    // cross cast.
    return NULL;  // ---- we do not want a valid cross-cast
  if (contained_nonvirtual_p (result.whole2src))
    // Src is known to be a non-public nonvirtual base of whole, and not a
    // base of dst. Found an invalid cross cast, which cannot also be a down
    // cast
    return NULL;
  if (result.dst2src == __class_type_info::__unknown)
    result.dst2src = dst_type->__find_public_src (-1, result.dst_ptr,
                                                  src_type, src_ptr);
  if (contained_public_p (result.dst2src))
    // Found a valid down cast
    return static_cast<DstPtr>(const_cast <void *> (result.dst_ptr));
  // Must be an invalid down cast, or the cross cast wasn't bettered
  return NULL;
}

template<typename DstRef, typename SrcParam>
requires (std::is_reference_v<DstRef> && !std::is_pointer_v< std::remove_reference_t<SrcParam> >)
DstRef down_cast(SrcParam &&src)  // src is a forwarding reference
{
  typedef SrcParam &&SrcRef;

  // Destination and Source must both be Lvalue, or both be Rvalue.
  static_assert(    std::is_lvalue_reference_v<DstRef> == std::is_lvalue_reference_v<SrcRef>
                 && std::is_rvalue_reference_v<DstRef> == std::is_rvalue_reference_v<SrcRef>,
                 "Mismatch in Lvalue and Rvalue references between Destination and Source" );

  typedef std::remove_reference_t<DstRef> Dst;
  typedef std::remove_reference_t<SrcRef> Src;

  // Destination type must be polymorphic
  if ( false == std::is_polymorphic_v<Dst> ) throw std::bad_cast{};
  // Source type must be a public base class of Destination type
  if ( false == std::derived_from<Dst,Src> ) throw std::bad_cast{};

  static_assert(    (std::is_const_v   <Dst> || !std::is_const_v<Src>   )
                 && (std::is_volatile_v<Dst> || !std::is_volatile_v<Src>),
                 "Mismatch in cv-qualifiers between Destination and Source" );

  Dst *const p = down_cast<Dst*>(&src);

  if ( nullptr == p ) throw std::bad_cast{};

  return *p;
}

2.2. Microsoft VC++

. . . TO DO  . . .

3. Impact on the Standard

The addition of std::down_cast constitutes a library extension that has no impact on existing language features or behavior.

4. Impact on Existing Code

No existing code becomes ill-formed due to the introduction of std::down_cast. The behavior of existing code is unaffected.

5. Acknowledgements

This was Phil Endecott’s idea.