Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add freestanding RTTI::to, RTTI::is, RTTI::isAny #4696

Merged
merged 4 commits into from
Jun 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/C++.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ we use a nested class with a reference member to the containing class
called `self`.

### RTTI
IR nodes and their handling code (e.g. Visitors) make extensive use of RTTI to perform type-checking and corresponding downcasting from `Node*` down to a particular implementation. C++ native RTTI as implemented in `dynamic_cast` and `typeid` built-ins is inherently slow and has lots of overhead. To circumvent this IR node classes use dedicated light-weight RTTI designed for semi-open class hierarchies. The implementation itself requires some boilerplate that is automatically generated by `ir-generator` for all IR classes from `.def`. The corresponding RTTI functionality could be accessed via `ICastable` interface from `lib/castable.h`. The lower-level RTTI implementation details are in `lib/rtti.h`. Overall, use of `dynamic_cast` and `typeid` for IR nodes is discouraged.
IR nodes and their handling code (e.g. Visitors) make extensive use of RTTI to perform type-checking and corresponding downcasting from `Node*` down to a particular implementation. C++ native RTTI as implemented in `dynamic_cast` and `typeid` built-ins is inherently slow and has lots of overhead. To circumvent this IR node classes use dedicated light-weight RTTI designed for semi-open class hierarchies. The implementation itself requires some boilerplate that is automatically generated by `ir-generator` for all IR classes from `.def`. The corresponding RTTI functionality could be accessed via `ICastable` interface from `lib/castable.h`. There are also freestandig helper functions and type traits in `lib/rtti_utils.h`, these helpers are mainly useful with generic algorithms and ranges. The lower-level RTTI implementation details are in `lib/rtti.h`. Overall, use of `dynamic_cast` and `typeid` for IR nodes is discouraged.

#### Using lightweight RTTI for other class hierarchies
Other class hierarchies besides `IR::Node` descendants could also benefit from lightweight RTTI functionality. In order to use it one needs to annotate the intended class hierarchy as follows:
Expand Down
147 changes: 147 additions & 0 deletions lib/rtti_utils.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*
Copyright 2024-present Intel Corporation.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

/// @file
/// @brief Utilities that help with use of custom-RTTI-enabled classes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe worth mentioning in docs/C++.md?


#ifndef LIB_RTTI_UTILS_H_
#define LIB_RTTI_UTILS_H_

#include <type_traits>

#include "rtti.h"

namespace RTTI {

/// A trait that check T is custom-RTTI-enabled. Works just like standard property type traits.
/// One would normally use the _v variant.
/// NOTE: Custom-RTTI-enabled classes should not only derive from RTTI::Base, but should also
/// declare typeId properly. However, not doing so would be a bug and use of such class in
/// RTTI-related operations would lead to static_assert failuire in RTTI::TypeInfo, so we just check
/// the base.
template <typename T>
struct has_rtti : std::is_base_of<RTTI::Base, T> {};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two pieces of RTTI in general:

  • One needs to be derived from RTTI:Base (plus all its bases must be derived from RTTI::Base)
  • One has appropriate methods implementation (e.g. via DECLARE_TYPEINFO-kind macro)

I think it's ok to check just for the base class here – TypeInfo will check for the whole set of constraints anyway should the RTTI methods are invoked.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I'd say being derived from RTTI::Base without the proper macros is definitely a bug, so firing a static_assert instead of the proper SFINAE-enabling failure in that case seems fine to me.


/// A trait that check T is custom-RTTI-enabled, variable version. Works just like standard property
/// type traits.
template <typename T>
inline constexpr const bool has_rtti_v = has_rtti<T>::value;

/// A type trait that check that all the types are custom-RTTI-enabled.
template <typename... Ts>
inline constexpr const bool all_have_rtti_v = (has_rtti_v<Ts> && ...);

/// A type condition for checking that T is custom-RTTI-enabled. A specialization of std::enable_if
/// and can be used in the same way.
/// @tparam T The type to be checked.
/// @tparam R The type that will be used for the ::type member if T is custom-rtti-enabled.
/// Defaults to void.
template <typename T, typename R = void>
struct enable_if_has_rtti : std::enable_if<has_rtti_v<T>, R> {};

/// A type condition for checking that all the types have RTTI. The resulting type is either not
/// defined if some of the types does not have RTTI (so a SFINAE use will fail), or it is defined to
/// be void. If you need another result type, please use std::enable_if<all_have_rtti_v<...>, T>.
template <typename... Ts>
using enable_if_all_have_rtti_t = std::enable_if_t<all_have_rtti_v<Ts...>, void>;

/// A type condition for checking that T is custom-RTTI-enabled. A specialization of
/// std::enable_if_t and can be used in the same way.
/// @tparam T The type to be checked.
/// @tparam R The type that will be used for the ::type member if T is custom-rtti-enabled.
/// Defaults to void.
template <typename T, typename R = void>
using enable_if_has_rtti_t = typename enable_if_has_rtti<T, R>::type;

namespace Detail {

template <typename To, typename = enable_if_has_rtti_t<To>>
struct ToType {
template <typename From, typename = enable_if_has_rtti_t<From>>
To *operator()(From *obj) const {
return obj ? obj->template to<To>() : nullptr;
}

template <typename From, typename = enable_if_has_rtti_t<From>>
const To *operator()(const From *obj) const {
return obj ? obj->template to<To>() : nullptr;
}
};

template <typename... Targets>
// TODO(C++20): use concepts to check enable_if_all_have_rtti_t<Targets...>> & that there is at
// least 1 target type.
struct IsType {
static_assert(sizeof...(Targets) > 0,
"At least one target type needs to be given for RTTI::is");
static_assert(all_have_rtti_v<Targets...>,
"All types in RTTI::is<Ts> need to be custom-rtti-enabled");

template <typename From, typename = enable_if_has_rtti_t<From>>
bool operator()(const From *obj) const {
return obj && (obj->template is<Targets>() || ...);
}
};

} // namespace Detail

/// A freestanding wrapper over From::to<T>(). It is an object, but is intended to be used a
/// function (similarly to C++20 range-implementing objects). Can be passed to functions expecting a
/// callable.
/// The callable has following signatures:
/// template<typename From, typename = enable_if_has_rtti_t<From>
/// * To *(From *)
/// * const To *(const From *)
///
/// Examples:
/// * RTTI::to<IR::Type>(n)
/// * std::copy_if(a.begin(), a.end(), std::back_inserter(b), RTTI::to<IR::Declaration>);
///
/// @tparam To The target type to cast to.
/// @returns nullptr if the from value is null or not of the To type, otherwise from cast to
/// To-type.
template <typename To>
inline const Detail::ToType<To> to;

/// A freestanding wrapper over From::is<T>(). Can be used to check if the value is of the
/// given type. A callable object with behaving as if it had the following signature:
/// template<typename From, typename = enable_if_has_rtti_t<From>
/// bool (From *)
///
/// Examples:
/// * if (RTTI::is<IR::Type::Bits>(typeOrNull)) { ... }
/// * std::find(x.begin(), x.end(), RTTI::is<IR::Declaration>);
///
/// @tparam Target The target type, from is checked to be of this type.
/// @returns true if the from object is not null and is of the target type, false otherwise.
template <typename Target>
inline const Detail::IsType<Target> is;

/// Similar to @ref RTTI::is, but accept accepts multiple types and succeeds if any of them matches
/// the type of the object.
///
/// Examples:
/// * if (RTTI::isAny<IR::Type::Bits>(typeOrNull)) { ... }
/// * std::find(x.begin(), x.end(), RTTI::isAny<IR::Declaration, IR::Type_Declaration>);
///
/// @tparam Targets The target type, from is checked to be of one of these types.
/// @returns true if the from object is not null and is of any of the target types, false otherwise.
template <typename... Targets>
inline const Detail::IsType<Targets...> isAny;

} // namespace RTTI

#endif // LIB_RTTI_UTILS_H_
70 changes: 70 additions & 0 deletions test/gtest/rtti_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ limitations under the License.
#include "ir/json_loader.h"
#include "ir/node.h"
#include "ir/vector.h"
#include "lib/rtti_utils.h"

namespace Test {

Expand Down Expand Up @@ -100,4 +101,73 @@ TEST(RTTI, JsonRestore) {
EXPECT_EQ(e2->typeId(), IR::NodeKind::Add);
}

TEST(RttiUtils, to) {
const auto *c = IR::Constant::get(IR::Type::Bits::get(4), 2);
const IR::Node *n = c;
const IR::Node *nullNode = nullptr;

EXPECT_EQ(RTTI::to<IR::Constant>(n), c);
EXPECT_EQ(RTTI::to<IR::Constant>(c), c);
EXPECT_EQ(RTTI::to<IR::Add>(c), nullptr);
EXPECT_EQ(RTTI::to<IR::Add>(nullNode), nullptr);

EXPECT_EQ(RTTI::to<IR::Literal>(n), n->to<IR::Literal>());
EXPECT_EQ(RTTI::to<IR::Type_Boolean>(c->type), nullptr);

std::vector<const IR::Node *> from{c, new IR::Add(c, c), n, new IR::LNot(c)};
std::vector<const IR::Operation *> to;
std::transform(from.begin(), from.end(), std::back_inserter(to), RTTI::to<IR::Operation>);

ASSERT_EQ(to.size(), 4);
EXPECT_EQ(to[0], nullptr);
ASSERT_NE(to[1], nullptr);
EXPECT_TRUE(to[1]->is<IR::Add>());
EXPECT_EQ(to[2], nullptr);
ASSERT_NE(to[3], nullptr);
EXPECT_TRUE(to[3]->is<IR::LNot>());
}

TEST(RttiUtils, is) {
const auto *c = IR::Constant::get(IR::Type::Bits::get(4), 2);
const IR::Node *n = c;
const IR::Node *nullNode = nullptr;

EXPECT_TRUE(RTTI::is<IR::Constant>(n));
EXPECT_TRUE(RTTI::is<IR::Constant>(c));
EXPECT_FALSE(RTTI::is<IR::Add>(c));
EXPECT_FALSE(RTTI::is<IR::BoolLiteral>(c));
EXPECT_FALSE(RTTI::is<IR::Add>(nullNode));

std::vector<const IR::Node *> from{c, new IR::Add(c, c), n, new IR::LNot(c)};
auto it = std::find_if(from.begin(), from.end(), RTTI::is<IR::Operation_Unary>);

EXPECT_NE(it, from.end());
EXPECT_EQ(it, std::prev(from.end()));
EXPECT_EQ(*it, from[3]);
}

TEST(RttiUtils, isAny) {
const auto *c = IR::Constant::get(IR::Type::Bits::get(4), 2);
const IR::Node *n = c;
const IR::Node *nullNode = nullptr;

EXPECT_TRUE(RTTI::isAny<IR::Constant>(n));
EXPECT_TRUE(RTTI::isAny<IR::Constant>(c));
EXPECT_FALSE(RTTI::isAny<IR::Add>(c));
EXPECT_FALSE(RTTI::isAny<IR::BoolLiteral>(c));
EXPECT_FALSE(RTTI::isAny<IR::Add>(nullNode));

EXPECT_TRUE((RTTI::isAny<IR::BoolLiteral, IR::Constant>(n)));
// EXPECT_TRUE(RTTI::isAny<>(n)); // does not compile, with is right
EXPECT_FALSE((RTTI::isAny<IR::Add, IR::LOr, IR::BAnd>(c)));

std::vector<const IR::Node *> from{c, new IR::Add(c, c), n, new IR::LNot(c)};
auto it = std::find_if(from.begin(), from.end(),
RTTI::isAny<IR::Operation_Unary, IR::Operation_Binary>);

EXPECT_NE(it, from.end());
EXPECT_EQ(it, std::next(from.begin()));
EXPECT_EQ(*it, from[1]);
}

} // namespace Test
Loading