Skip to content

Commit

Permalink
Finish the first draft of RTTI support with tests and docs
Browse files Browse the repository at this point in the history
  • Loading branch information
pavel-kirienko committed Feb 19, 2024
1 parent 6dc1e18 commit fd4e933
Show file tree
Hide file tree
Showing 2 changed files with 196 additions and 79 deletions.
148 changes: 126 additions & 22 deletions cetlvast/suites/unittest/test_rtti.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,23 @@
#include <cetl/rtti.hpp>
#include <gtest/gtest.h>

#define CETL_RTTI(base, ...) \
public: \
static constexpr cetl::type_id _type_id_{__VA_ARGS__}; \
\
protected: \
cetl::type_id _get_type_id_() const noexcept override \
{ \
return _type_id_; \
} \
void* _cast_(const cetl::type_id& id) noexcept override \
{ \
return (id == _type_id_) ? static_cast<void*>(this) : base::_cast_(id); \
} \
const void* _cast_(const cetl::type_id& id) const noexcept override \
{ \
return (id == _type_id_) ? static_cast<const void*>(this) : base::_cast_(id); \
} \
\
private:
#define CETL_RTTI(base, ...) \
static constexpr cetl::type_id _get_static_type_id_() noexcept \
{ \
return cetl::type_id{__VA_ARGS__}; \
} \
cetl::type_id _get_polymorphic_type_id_() const noexcept override \
{ \
return _get_static_type_id_(); \
} \
void* _cast_(const cetl::type_id& id)& noexcept override \
{ \
return (id == _get_static_type_id_()) ? static_cast<void*>(this) : base::_cast_(id); \
} \
const void* _cast_(const cetl::type_id& id) const& noexcept override \
{ \
return (id == _get_static_type_id_()) ? static_cast<const void*>(this) : base::_cast_(id); \
}

struct A : cetl::rtti
{
Expand All @@ -49,14 +47,120 @@ struct C : B

struct D final
{
static constexpr cetl::type_id _type_id_{
{0x3, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF}};
const char value = 'd';
static constexpr cetl::type_id _get_static_type_id_() noexcept
{
return {0x3, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF};
}
};

TEST(test_rtti, basic)
{
EXPECT_EQ(cetl::get_type_id(A{}), A::_get_static_type_id_());
EXPECT_EQ(cetl::get_type_id(B{}), B::_get_static_type_id_());
EXPECT_EQ(cetl::get_type_id(C{}), C::_get_static_type_id_());
EXPECT_EQ(cetl::get_type_id(D{}), D::_get_static_type_id_());

EXPECT_EQ(nullptr, cetl::rtti_cast<D*>(static_cast<A*>(nullptr)));
EXPECT_EQ(nullptr, cetl::rtti_cast<A*>(static_cast<C*>(nullptr)));
EXPECT_EQ(nullptr, cetl::rtti_cast<D*>(static_cast<const A*>(nullptr)));
EXPECT_EQ(nullptr, cetl::rtti_cast<A*>(static_cast<const C*>(nullptr)));

A a;
EXPECT_EQ(cetl::get_type_id(a),
cetl::type_id({0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF}));
EXPECT_EQ(cetl::get_type_id(a), a._get_static_type_id_());
EXPECT_EQ('a', cetl::rtti_cast<A*>(&a)->value);
EXPECT_EQ('a', cetl::rtti_cast<A*>(static_cast<const A*>(&a))->value);
EXPECT_EQ(&a, cetl::rtti_cast<A*>(&a));
EXPECT_EQ(&a, cetl::rtti_cast<A*>(static_cast<const A*>(&a)));
EXPECT_EQ(nullptr, cetl::rtti_cast<B*>(&a));
EXPECT_EQ(nullptr, cetl::rtti_cast<B*>(static_cast<const A*>(&a)));
}

TEST(test_rtti, polymorphism)
{
using cetl::rtti_cast;
using cetl::is_instance_of;

B b;
C c;

EXPECT_EQ(cetl::get_type_id(b),
cetl::type_id({0x1, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF}));
EXPECT_EQ(cetl::get_type_id(c),
cetl::type_id({0x2, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF}));
EXPECT_EQ(cetl::get_type_id(b), b._get_static_type_id_());
EXPECT_EQ(cetl::get_type_id(c), c._get_static_type_id_());

// identity, b to b
EXPECT_EQ('b', rtti_cast<B*>(&b)->value);
EXPECT_EQ('b', rtti_cast<B*>(static_cast<const B*>(&b))->value);
EXPECT_EQ(&b, rtti_cast<B*>(&b));
EXPECT_EQ(&b, rtti_cast<B*>(static_cast<const B*>(&b)));

// identity, c to c
EXPECT_EQ('c', rtti_cast<C*>(&c)->value);
EXPECT_EQ('c', rtti_cast<C*>(static_cast<const C*>(&c))->value);
EXPECT_EQ(&c, rtti_cast<C*>(&c));
EXPECT_EQ(&c, rtti_cast<C*>(static_cast<const C*>(&c)));

// up-conversion, b to a
EXPECT_EQ('a', rtti_cast<A*>(&b)->value);
EXPECT_EQ('a', rtti_cast<A*>(static_cast<const B*>(&b))->value);
EXPECT_EQ(&b, rtti_cast<A*>(&b));
EXPECT_EQ(&b, rtti_cast<A*>(static_cast<const B*>(&b)));

// up-conversion, c to b
EXPECT_EQ('b', rtti_cast<B*>(&c)->value);
EXPECT_EQ('b', rtti_cast<B*>(static_cast<const C*>(&c))->value);
EXPECT_EQ(&c, rtti_cast<B*>(&c));
EXPECT_EQ(&c, rtti_cast<B*>(static_cast<const C*>(&c)));

// up-conversion, c to a
EXPECT_EQ('a', rtti_cast<A*>(&c)->value);
EXPECT_EQ('a', rtti_cast<A*>(static_cast<const C*>(&c))->value);
EXPECT_EQ(&c, rtti_cast<A*>(&c));
EXPECT_EQ(&c, rtti_cast<A*>(static_cast<const C*>(&c)));

A& a_b = b;
A& a_c = c;

// down-conversion, a to b
EXPECT_EQ('b', rtti_cast<B*>(&a_b)->value);
EXPECT_EQ('b', rtti_cast<B*>(static_cast<const A*>(&a_b))->value);
EXPECT_EQ(&b, rtti_cast<B*>(&a_b));
EXPECT_EQ(&b, rtti_cast<B*>(static_cast<const A*>(&a_b)));

// down-conversion, a to c
EXPECT_EQ('c', rtti_cast<C*>(&a_c)->value);
EXPECT_EQ('c', rtti_cast<C*>(static_cast<const A*>(&a_c))->value);
EXPECT_EQ(&c, rtti_cast<C*>(&a_c));
EXPECT_EQ(&c, rtti_cast<C*>(static_cast<const A*>(&a_c)));

// illegal down-conversion, b to c
EXPECT_EQ(nullptr, rtti_cast<C*>(&a_b));
EXPECT_EQ(nullptr, rtti_cast<C*>(static_cast<const A*>(&a_b)));
EXPECT_EQ(nullptr, rtti_cast<C*>(&b));
EXPECT_EQ(nullptr, rtti_cast<C*>(static_cast<const B*>(&b)));

// is_instance_of
EXPECT_TRUE(is_instance_of<A>(b));
EXPECT_TRUE(is_instance_of<B>(b));
EXPECT_FALSE(is_instance_of<C>(b));
EXPECT_FALSE(is_instance_of<D>(b));

EXPECT_TRUE(is_instance_of<A>(c));
EXPECT_TRUE(is_instance_of<B>(c));
EXPECT_TRUE(is_instance_of<C>(c));
EXPECT_FALSE(is_instance_of<D>(c));

EXPECT_TRUE(is_instance_of<A>(a_b));
EXPECT_TRUE(is_instance_of<B>(a_b));
EXPECT_FALSE(is_instance_of<C>(a_b));
EXPECT_FALSE(is_instance_of<D>(a_b));

EXPECT_TRUE(is_instance_of<A>(a_c));
EXPECT_TRUE(is_instance_of<B>(a_c));
EXPECT_TRUE(is_instance_of<C>(a_c));
EXPECT_FALSE(is_instance_of<D>(a_c));
}
127 changes: 70 additions & 57 deletions include/cetl/rtti.hpp
Original file line number Diff line number Diff line change
@@ -1,16 +1,7 @@
/// @file
///
/// An alternative implementation of simple runtime type information (RTTI) capability designed for high-integrity
/// real-time systems, where the use of the standard C++ RTTI is discouraged.
///
/// Unlike the standard C++ RTTI, this implementation allows the user to manually specify the type ID per type
/// in the form of a 16-byte UUID (GUID), query runtime type information in constant time,
/// and perform safe dynamic type conversion in constant time (like \c dynamic_cast).
///
/// In order to support this RTTI capability, a type must opt in by at least defining a public
/// static constexpr member named \c _type_id_ of type \ref cetl::type_id; if the type is polymorphic,
/// it should also implement the \ref cetl::rtti interface (more on this in the documentation of said interface).
///
/// @copyright
/// Copyright (C) OpenCyphal Development Team <opencyphal.org>
/// Copyright Amazon.com Inc. or its affiliates.
Expand All @@ -19,7 +10,6 @@
#ifndef CETL_RTTI_HPP_INCLUDED
#define CETL_RTTI_HPP_INCLUDED

#include <cetl/pf17/optional.hpp> // This could be optionally replaced with cetlpf.hpp.
#include <cetl/pf17/attribute.hpp>

#include <array>
Expand All @@ -28,63 +18,74 @@

namespace cetl
{
/// This many bytes are used to represent a type ID. This is enough to hold a standard UUID (GUID).
constexpr std::size_t type_id_size = 16;

/// A 16-byte UUID (GUID) that uniquely identifies a type.
/// The user is responsible for ensuring that each type that has opted into this RTTI capability has a unique type ID
/// exposed via a static constexpr member named \c _type_id_ of this type.
/// exposed via a public method <tt>static constexpr cetl::type_id _get_static_type_id_() noexcept</tt>.
using type_id = std::array<std::uint8_t, type_id_size>;

/// A null option is used to represent a type that has not opted into this RTTI capability (undefined type ID).
using maybe_type_id = pf17::optional<type_id>;

namespace detail
{
template <typename T>
auto has_type_id_impl(int) -> decltype(std::declval<std::decay_t<T>&>()._type_id_, std::true_type{});
auto has_type_id_impl(int) -> decltype(std::declval<std::decay_t<T>&>()._get_static_type_id_(), std::true_type{});
template <typename>
std::false_type has_type_id_impl(...);
} // namespace detail

/// True iff \c T has a public field named \c _type_id_ of type \ref cetl::type_id;
/// i.e., \c T supports the RTTI capability.
/// True iff \c T has a public static method \c _get_static_type_id_().
template <typename T>
constexpr bool has_type_id = decltype(detail::has_type_id_impl<std::decay_t<T>>(0))::value;

/// A polymorphic interface that allows the user to query runtime type information and perform safe dynamic type
/// conversion in constant time.
/// An alternative implementation of simple runtime type information (RTTI) capability designed for high-integrity
/// real-time systems, where the use of the standard C++ RTTI is discouraged.
///
/// Unlike the standard C++ RTTI, this implementation allows/requires the user to manually specify the type ID per type
/// in the form of a 16-byte UUID (GUID), query runtime type information in constant time,
/// and perform safe dynamic type down-conversion in constant time (similar to \c dynamic_cast).
/// The limitations are that a type has to opt into this RTTI capability explicitly and that CV-qualifiers
/// are not considered in the type comparison.
///
/// In order to support this RTTI implementation, a type must opt in by at least defining a public
/// static constexpr member named \c _type_id_ of type \ref cetl::type_id; if the type is polymorphic,
/// it should also implement this interface.
/// In order to support this RTTI capability, a type must opt in by at least defining a public method
/// <tt>static constexpr cetl::type_id _get_static_type_id_() noexcept</tt>;
/// such types satisfy \ref cetl::has_type_id.
/// If the type is polymorphic, it should also implement this interface.
///
/// The members of this type are named with surrounding underscores to avoid name clashes with user-defined members,
/// The static \c _get_static_type_id_() method returns the type ID of the type it is defined on.
/// It could be a static member variable as well, but in C++14 dealing with static members
/// of non-literal types is not as straightforward as in C++17, so we use a static method instead.
/// The virtual \ref rtti::_get_polymorphic_type_id_() method forwards the call to the static
/// \c _get_static_type_id_() method of the actual type of the object;
/// see the method documentation for details.
///
/// RTTI-related members are named with surrounding underscores to avoid name clashes with user-defined members,
/// including the members of DSDL types (where identifiers surrounded with underscores are reserved).
class rtti
{
friend type_id get_type_id(const rtti& obj) noexcept;

template <typename T, typename P, std::enable_if_t<std::is_pointer<T>::value && has_type_id<P>, int>>
friend T rtti_cast(class rtti*) noexcept;
friend T rtti_cast(rtti*) noexcept;
template <typename T, typename P, std::enable_if_t<std::is_pointer<T>::value && has_type_id<P>, int>>
friend const P* rtti_cast(const class rtti*) noexcept;
friend const P* rtti_cast(const rtti*) noexcept;

friend bool is_instance_of(const rtti& obj, const type_id& id) noexcept;

protected:
/// The method body should be implemented as follows,
/// where \c _type_id_ is a static constexpr member of type \ref cetl::type_id:
/// The method body should simply forward the result of this type's static \c _get_static_type_id_ method:
///
/// @code
/// return _type_id_;
/// return _get_static_type_id_();
/// @endcode
///
/// This method should not be invoked directly; instead, use the \ref cetl::get_type_id function.
CETL_NODISCARD virtual type_id _get_type_id_() const noexcept = 0;
/// This method should not be invoked directly; instead, use \ref cetl::get_type_id.
CETL_NODISCARD virtual type_id _get_polymorphic_type_id_() const noexcept = 0;

/// The method body should be implemented as follows,
/// where \c _type_id_ is a static constexpr member of type \ref cetl::type_id:
/// The method body should be implemented as follows (add \c const in the const overload):
///
/// @code
/// return (id == _type_id_) ? this : base::_cast_(id);
/// return (id == _get_static_type_id_()) ? static_cast<void*>(this) : base::_cast_(id);
/// @endcode
///
/// Where \c base is the base class of the current class that implements this method.
Expand All @@ -93,14 +94,14 @@ class rtti
/// simply returns a null pointer, indicating that the search has returned no matches and a
/// safe dynamic type conversion is not possible.
///
/// This method should not be invoked directly; instead, use the \ref cetl::rtti_cast function.
/// This method should not be invoked directly; instead, use \ref cetl::rtti_cast.
/// @{
CETL_NODISCARD virtual void* _cast_(const type_id& id) noexcept
CETL_NODISCARD virtual void* _cast_(const type_id& id) & noexcept
{
(void) id;
return nullptr;
}
CETL_NODISCARD virtual const void* _cast_(const type_id& id) const noexcept
CETL_NODISCARD virtual const void* _cast_(const type_id& id) const& noexcept
{
(void) id;
return nullptr;
Expand All @@ -115,54 +116,66 @@ class rtti
rtti& operator=(rtti&&) = default;
};

/// Returns the type ID of the given type.
/// This function is provided for regularity; it simply forwards the call to \c T::_get_static_type_id_().
/// The type shall satisfy \ref cetl::has_type_id.
template <typename T>
CETL_NODISCARD constexpr type_id get_type_id() noexcept
{
return std::decay_t<T>::_get_static_type_id_();
}
/// Returns the type ID of the given object.
/// This overload is for objects that implement the \ref cetl::rtti interface; it simply forwards the result of the
/// \ref cetl::rtti::_get_type_id_ method.
///
/// Generic code is recommended to convert the result of this function into \ref maybe_type_id.
/// \ref cetl::rtti::_get_polymorphic_type_id_() method.
CETL_NODISCARD inline type_id get_type_id(const rtti& obj) noexcept
{
return obj._get_type_id_();
return obj._get_polymorphic_type_id_();
}
/// Returns the type ID of the given object.
/// This overload is selected for objects that do not implement the \ref cetl::rtti interface but provide a static
/// constexpr member named \c _type_id_ of type \ref cetl::type_id.
///
/// Generic code is recommended to convert the result of this function into \ref maybe_type_id.
/// This overload is selected for objects that do not implement the \ref cetl::rtti interface but satisfy
/// \ref cetl::has_type_id.
template <typename T, std::enable_if_t<has_type_id<T>, int> = 0>
CETL_NODISCARD constexpr type_id get_type_id(const T& obj) noexcept
{
return obj._type_id_;
}
/// Returns the type ID of the given object. This is a fallback overload for objects that are not RTTI-capable;
/// it always returns an empty option.
template <typename T, std::enable_if_t<!has_type_id<T>, int> = 0>
CETL_NODISCARD constexpr maybe_type_id get_type_id(const T&) noexcept
CETL_NODISCARD constexpr type_id get_type_id(const T&) noexcept
{
return {};
return get_type_id<T>();
}

/// Performs a safe dynamic type conversion in constant time by invoking \ref cetl::rtti::_cast_.
/// T shall be a pointer of a type that contains a public static constexpr member named \c _type_id_ of type
/// \ref cetl::type_id.
/// T shall satisfy \ref cetl::has_type_id.
/// Returns nullptr if a safe dynamic type conversion to T is not possible.
/// @{
template <typename T,
typename P = std::remove_pointer_t<T>,
std::enable_if_t<std::is_pointer<T>::value && has_type_id<P>, int> = 0>
CETL_NODISCARD T rtti_cast(rtti* const obj) noexcept
{
return (obj == nullptr) ? nullptr : static_cast<T>(obj->_cast_(P::_type_id_));
return (obj == nullptr) ? nullptr : static_cast<T>(obj->_cast_(get_type_id<P>()));
}
template <typename T,
typename P = std::remove_pointer_t<T>,
std::enable_if_t<std::is_pointer<T>::value && has_type_id<P>, int> = 0>
CETL_NODISCARD const P* rtti_cast(const rtti* const obj) noexcept
{
return (obj == nullptr) ? nullptr : static_cast<const P*>(obj->_cast_(P::_type_id_));
return (obj == nullptr) ? nullptr : static_cast<const P*>(obj->_cast_(get_type_id<P>()));
}
/// @}

/// Indicates whether the given polymorphic object is an instance of the type with the given type ID.
/// For example, given a polymorphic type hierarchy <tt>A<-B<-C</tt>,
/// \c is_instance_of(C{}, get_type_id<X>()) is true for \c X in \c {A,B,C}.
CETL_NODISCARD inline bool is_instance_of(const rtti& obj, const type_id& id) noexcept
{
return nullptr != obj._cast_(id);
}
/// Indicates whether the given polymorphic object is an instance of the given type.
/// T shall satisfy \ref cetl::has_type_id.
/// Refer to the non-template overload for more details.
template <typename T>
CETL_NODISCARD bool is_instance_of(const rtti& obj) noexcept
{
return is_instance_of(obj, get_type_id<T>());
}

} // namespace cetl

#endif // CETL_RTTI_HPP_INCLUDED

0 comments on commit fd4e933

Please sign in to comment.