taichi-dev / cpp-training-season1

C++ training, season 1
Apache License 2.0
53 stars 13 forks source link

reflection运行时候报错 #2

Open wesleywong1995 opened 1 year ago

wesleywong1995 commented 1 year ago

测试环境,windows, unbuntu wsl。 在RawTypeDescriptorBuilder进行析构注册的时候desc_变量报空,是否是析构顺序的问题?

k-ye commented 1 year ago

Thanks for reporting this! Were you just running https://github.com/taichi-dev/cpp-training-season1/blob/main/reflection/src/main.cpp?

There's nothing fancy in ~RawTypeDescriptorBuilder(): https://github.com/taichi-dev/cpp-training-season1/blob/bfe7b2f35a4517635545aea9cf74a0f7097dbaa3/reflection/src/reflect.cpp#L13-L15. If you print desc_'s address before L14, is it already nullptr?

CheapMeow commented 4 months ago

It is a common bug about move constructor and copy assignment operator. In these constructor, std::unique_ptr<T> member is moved from rhs to lhs, so it is empty in rhs.

In code, AddClass return a instance of TypeDescriptorBuilder<T>, it calls move constructor.

// reflect.hpp

template <typename T>
details::TypeDescriptorBuilder<T> AddClass(const std::string &name) {
  details::TypeDescriptorBuilder<T> b{name};
  return b;
}

Definition of TypeDescriptorBuilder<T> doesn't implement move constructor, so the default one is called.

// reflect.hpp

template <typename T>
class TypeDescriptorBuilder {
 ...

 private:
  RawTypeDescriptorBuilder raw_builder_;
};

Default move ctor of TypeDescriptorBuilder<T> calls move constructor of RawTypeDescriptorBuilder.

// reflect.hpp

class RawTypeDescriptorBuilder {
  ...

  RawTypeDescriptorBuilder(RawTypeDescriptorBuilder&& rhs) = default;
  RawTypeDescriptorBuilder& operator=(RawTypeDescriptorBuilder&& rhs) = default;

 ...

 private:
  std::unique_ptr<TypeDescriptor> desc_{nullptr};
};

Default move ctor of RawTypeDescriptorBuilder moves std::unique_ptr<TypeDescriptor> desc_.

Now go back to details::TypeDescriptorBuilder<T> AddClass(const std::string &name), after move ctor, the lhs now owns the std::unique_ptr member, the rhs owns empty.

When the program leaves details::TypeDescriptorBuilder<T> AddClass(const std::string &name), the rhs is deleted.

To output the process, modify the code like this:

// reflect.hpp

  template <typename V>
  TypeDescriptorBuilder &AddMemberVar(const std::string &name, V T::*var) {
    std::cout << "AddMemberVar" << std::endl;
    raw_builder_.AddMemberVar(name, var);
    return *this;
  }
// reflect.cpp

RawTypeDescriptorBuilder::~RawTypeDescriptorBuilder() {
    std::cout << "~RawTypeDescriptorBuilder()" << std::endl;
    std::cout << "(desc_ == nullptr) = " << (desc_ == nullptr) << std::endl;
    if(desc_ != nullptr)
        Registry::instance().Register(std::move(desc_));
}

The output is

~RawTypeDescriptorBuilder()
(desc_ == nullptr) = 1
AddMemberVar
AddMemberVar
~RawTypeDescriptorBuilder()
(desc_ == nullptr) = 0

Obviously, the fix is

// reflect.cpp

RawTypeDescriptorBuilder::~RawTypeDescriptorBuilder() {
    if(desc_ != nullptr)
        Registry::instance().Register(std::move(desc_));
}