rttrorg / rttr

C++ Reflection Library
https://www.rttr.org
MIT License
3.11k stars 429 forks source link

Dealing with covariant arguments #335

Open tr8dr opened 2 years ago

tr8dr commented 2 years ago

I have a situation where I want to call a function via rttr reflection where one of the arguments is a pointer to a base class:

method (shared_ptr<Base>, ...)

Elsewhere a number of different subclasses may be created with an implied shared_ptr<Derived1>, shared_ptr<Derived2> and so on. These are presented in an argument vector and invoked as:

meth.invoke_variadic(rttr::instance(), argv)

RTTR does not recognize that the rttr::argument's of shared_ptr<Derived> in the argument vector are covariant with shared_ptr<Base>. I am not in control of how these derived classes are created or presented, so would like to have a way of up-casting these to the base shared_ptr OR alternatively have RTTR recognize the covariance and do this under the covers.

Is there a facility to deal with this?

shinton-ps commented 1 year ago

Hi tr8dr, and anyone else who sees this and had the same question.

I just started investigating this library yesterday, so I am certainly not well versed on its internals. I took a look at the unit tests in src/unit_tests/variant/variant_conv_test.cpp. In the last test case, you can see an example of how you might achieve what you're asking:

TEST_CASE("variant test - register_wrapper_converter_for_base_classes<std::shared_ptr<T>>", "[variant]")
{
    variant var = std::make_shared<derived>();
    CHECK(var.convert(type::get<std::shared_ptr<base>>())           == false);

    type::register_wrapper_converter_for_base_classes<std::shared_ptr<derived>>();

    CHECK(var.convert(type::get<std::shared_ptr<base>>())           == true);
    CHECK(var.convert(type::get<std::shared_ptr<derived>>())        == true);

    type::register_wrapper_converter_for_base_classes<std::shared_ptr<other_derived>>();

    // negative test, we need first make a down cast, otherwise the target_type converter cannot be found
    CHECK(var.convert(type::get<std::shared_ptr<base>>())           == true);
    CHECK(var.convert(type::get<std::shared_ptr<other_derived>>())  == false);
}

The issue with this is that this still requires you know the type up front to supply in the type::get template argument.

It seems that RTTR can't automatically resolve a variant as a base class, but I did find a very dirty way to get it working. This makes me very sad, but here it is (using spdlog in my scratch environment):

#include "rttr/registration"
#include "spdlog/spdlog.h"

class Base {
  RTTR_ENABLE()
 public:
  void BaseDummy() { spdlog::info("Called Base::Dummy"); }
};

class Other {
  RTTR_ENABLE()
 public:
  void OtherDummy() { spdlog::info("Called Other::Dummy"); }
};

class TestClass : public Base, public Other {
  RTTR_ENABLE(Base, Other)
 public:
  void SetOtherTestClass(const std::shared_ptr<TestClass>& other) {
    spdlog::debug("SetOtherTestClass");
    OtherTestClass_ = other;
  }

  void SetOtherClass(const std::shared_ptr<Other>& other) {
    spdlog::debug("SetOtherClass");
    Other_ = other;
  }

  void SetBaseClass(const std::shared_ptr<Base>& other) {
    spdlog::debug("SetBaseClass");
    Base_ = other;
  }

  void PrintValues() {
    if (Other_) {
      Other_->OtherDummy();
    }
    if (Base_) {
      Base_->BaseDummy();
    }
    if (OtherTestClass_) {
      OtherTestClass_->PrintValues();
    }
  }

 private:
  std::shared_ptr<TestClass> OtherTestClass_;
  std::shared_ptr<Base> Base_;
  std::shared_ptr<Other> Other_;
};

RTTR_REGISTRATION {
  rttr::registration::class_<TestClass>("TestClass")
      .constructor<>()
      .method("PrintValues", &TestClass::PrintValues)
      .method("SetOtherTestClass", &TestClass::SetOtherTestClass)
      .method("GetOtherTestClass", &TestClass::GetOtherTestClass)
      .method("SetOtherClass", &TestClass::SetOtherClass)
      .method("SetBaseClass", &TestClass::SetBaseClass);
   // This is the key to upcast/downcast in the hierarchy:
  rttr::type::register_wrapper_converter_for_base_classes<std::shared_ptr<TestClass>>();
};

// Wrapper to print error message when class method doesn't exist.
rttr::method GetMethod(const rttr::type& t, const std::string& name) {
  rttr::method m = t.get_method(name);
  if (!m.is_valid()) {
    spdlog::error("{} is not a valid member function of {}", name, std::string(t.get_name()));
  }
  return m;
}

int main() {
  spdlog::set_level(spdlog::level::debug);

  rttr::type test_class_type     = rttr::type::get_by_name("TestClass");
  rttr::variant test_class       = test_class_type.create();
  rttr::variant other_test_class = test_class_type.create();

  rttr::method PrintValues       = GetMethod(test_class_type, "PrintValues");
  rttr::method SetOtherTestClass = GetMethod(test_class_type, "SetOtherTestClass");
  rttr::method SetOtherClass     = GetMethod(test_class_type, "SetOtherClass");
  rttr::method SetBaseClass      = GetMethod(test_class_type, "SetBaseClass");

  rttr::variant result;

  result = SetOtherTestClass.invoke(test_class, other_test_class);
  if (!result.is_valid()) {
    spdlog::error("Failed to set other test class.");
  }

  const rttr::type& original_type = other_test_class.get_type();

  // I'm not proud of this, but this is my quick and dirty way to demonstrate the usage of the library to solve
  // our shared problem:
  for (const auto& param : SetOtherClass.get_parameter_infos()) {
    if (other_test_class.can_convert(param.get_type())) {
      other_test_class.convert(param.get_type());
      result = SetOtherClass.invoke(test_class, other_test_class);
      if (!result.is_valid()) {
        spdlog::error("Failed to set other class.");
      }
    } else {
      spdlog::error("Cannot convert {} to {}.", std::string(other_test_class.get_type().get_name()),
                    std::string(param.get_name()));
    }
    break;
  }

  spdlog::debug("After SetOtherClass other_test_class variant is a {}",
                std::string(other_test_class.get_type().get_name()));

  // Convert back to parent class. This works, but not ideal because requires knowing type.
  // other_test_class.convert(rttr::type::get<std::shared_ptr<TestClass>>());

  // Convert back to parent class. This works.
  other_test_class.convert(original_type);

  // Convert back to parent class. This doesn't work.
  // other_test_class.convert(rttr::type::get_by_name("TestClass"));

  spdlog::debug("After convert other_test_class variant is a {}", std::string(other_test_class.get_type().get_name()));

  // Repeating the same awful pattern as above, but for the 'Base' class type.
  for (const auto& param : SetBaseClass.get_parameter_infos()) {
    if (other_test_class.can_convert(param.get_type())) {
      other_test_class.convert(param.get_type());
      result = SetBaseClass.invoke(test_class, other_test_class);
      if (!result.is_valid()) {
        spdlog::error("Failed to set other class.");
      }
    } else {
      spdlog::error("Cannot convert {} to {}.", std::string(other_test_class.get_type().get_name()),
                    std::string(param.get_name()));
    }
    break;
  }

  result = PrintValues.invoke(test_class);
  if (!result.is_valid()) {
    spdlog::error("Failed to print values.");
  }

And here is my output:

[2022-12-13 16:41:56.616] [debug] SetOtherTestClass
[2022-12-13 16:41:56.616] [debug] SetOtherClass
[2022-12-13 16:41:56.616] [debug] After SetOtherClass other_test_class variant is a std::shared_ptr<Other>
[2022-12-13 16:41:56.616] [debug] After convert other_test_class variant is a std::shared_ptr<TestClass>
[2022-12-13 16:41:56.616] [debug] SetBaseClass
[2022-12-13 16:41:56.617] [info] Called Other::Dummy
[2022-12-13 16:41:56.617] [info] Called Base::Dummy

Big kudos to Axel for creating this library!