从lambda返回的对象丢失属性值

3

我正在学习一门数年前的课程,第一次学习如何使用序列化。

当 lambda 表达式中执行 "return result" 时,Contact 的 Address 属性变成未初始化状态。注释掉的代码可以正常工作,所以我相当确定我编译 Boost 库没有问题。

Contact 的名称返回正常,为什么地址不行呢?

#include <string>
#include <iostream>
#include <memory>
#include <functional>
#include <sstream>
using namespace std;

#include <boost/serialization/serialization.hpp>
#include <boost/archive/text_iarchive.hpp>
#include <boost/archive/text_oarchive.hpp>

struct Address
{
public:
    string street, city;
    int suite;

    Address() {};
    Address(string street, string city, int suite)
        : suite(suite),street(street),city(city){ }

  friend ostream& operator<<(ostream& os, const Address& obj)
  {
    return os
      << "street: " << obj.street
      << " city: " << obj.city
      << " suite: " << obj.suite;
  }

private:
  friend class boost::serialization::access;

  template<class Ar> void serialize(Ar& ar, const unsigned int version)
  {
    ar & street;
    ar & city; 
    ar & suite;
  }
};

struct Contact
{
  string name;
  Address* address;


  friend ostream& operator<<(ostream& os, const Contact& obj)
  {
    return os
      << "name: " << obj.name
      << " address: " << *obj.address;
  }
private:
  friend class boost::serialization::access;

  template<class Ar> void serialize(Ar& ar, const unsigned int version)
  {
    ar & name;
    ar & address;
  }
};

int main()
{
  Contact john;
  john.name = "John Doe";
  john.address = new Address{ "123 East Dr", "London", 123 };

  auto clone = [](Contact c)
  {
    ostringstream oss;
    boost::archive::text_oarchive oa(oss);
    oa << c;

    string s = oss.str();

    Contact result;
    istringstream iss(s);
    boost::archive::text_iarchive ia(iss);
    ia >> result;
    return result;
  };

  // This works fine
  //ostringstream oss;
  //boost::archive::text_oarchive oa(oss);
  //oa << john;

  //string s = oss.str();

  //Contact newJane;
  //{
     // istringstream iss(s);
     // boost::archive::text_iarchive ia(iss);
     // ia >> newJane;
  //}

  //newJane.name = "Jane";
  //newJane.address->street = "123B West Dr";

  //cout << john << endl << newJane << endl;


  Contact jane = clone(john);
  jane.name = "Jane";
  jane.address->street = "123B West Dr";

  cout << john << endl << jane << endl;

  getchar();
  return 0;
}

首先,实现五个规则。目前,联系人执行其地址指针的浅拷贝(我正在使用移动设备,所以这大概就是我能为您做的全部)。 - AndyG
@AndyG 没错,那就是需要的。讲师使用了上面的代码并且它起作用了。我试图复制他所做的。请发布为答案,以便我可以接受它。 - AC Thompson
你为什么要使用指针来操作地址? - Caleth
2个回答

1

Contact没有重载拷贝构造函数,因此默认构造函数会被生成。

默认的拷贝构造函数使用它们的默认构造函数来复制所有(非static)成员变量。具体来说,指针的默认构造函数只复制存储的地址。

因此,使用Contact的拷贝构造函数,将构造一个新实例,其中Contact::address指向与原始实例完全相同的Address

因此,更改jane的地址也会更改joe的地址。

这可能是有意或无意的:

  • 如果需要共享资源,则是有意的
  • 如果Contact应该独占其address,则是无意的。

如果janejoe没有结婚,则可能是无意的。

在当前状态下,设计还有另一个缺陷:

当销毁Contact时,哪个实例负责删除address指向的对象?

如果将~Contact()添加到析构函数中,情况会变得更糟。(删除jane将删除她的地址,并留下一个悬空指针给john。)
现在,销毁一个Contact可能会导致内存泄漏。(外部代码必须负责删除Address的剩余实例。这很难维护。)
这样的设计问题并不罕见,并导致了三五法则的出现:
如果其中一个
- 析构函数 - 拷贝构造函数 - 拷贝赋值运算符
被显式定义,那么其他两个也很可能会被定义。
随着C++11(引入移动语义),这被扩展为五法则,增加了
- 移动构造函数 - 移动赋值运算符。
因此,一个显式定义可以简单地将它们删除:
struct Contact {
  Address *address;

  // default constructor with initialization to empty
  Contact(): address(new Address()) { }

  // constructor with values
  Contact(const Address &address): address(new Address(address)) { }

  // destructor.
  ~Contact()
  {
    delete address; // prevent memory leak
  }

  // move constructor.
  Contact(Contact &&contact): address(contact.address)
  {
    contact.address = nullptr; // prevent two owners
  }
  // move assignment.
  Contact& operator=(Contact &&contact)
  {
    address = contact.address;
    contact.address = nullptr; // prevent two owners
    return *this;
  }

  // prohibited:
  Contact(const Contact&) = delete;
  Contact& operator=(const Contact&) = delete;
};

这是一个有关内存管理的改进,但对于克隆Contact实例的意图来说是适得其反的。
另一个可能的解决方案是将address存储为std::shared_ptr<Address>而不是Address*std::shared_ptr(智能指针之一)已经被引入用于此类问题(涉及共享所有权)。
struct Contact {
  std::shared_ptr<Address> address;

  // default constructor with initialization to empty
  Contact(): address(std::make_shared<Address>()) { }

  // constructor with values
  Contact(const Address &address):
    address(std::make_shared<Address>(address))
  { }

  // another constructor with values
  Contact(const std::shared_ptr<Address> &address):
    address(address)
  { }

  // destructor.
  ~Contact() = default;
  /* default destructor of shared_ptr is fully sufficient
   * It deletes the pointee just if there are no other shared_ptr's to it.
   */

  // copy constructor.
  Contact(const Contact&) = default; // copies shared_ptr by default
  // copy assignment.
  Contact& operator=(const Contact&) = default; // copies shared_ptr by default
  // move constructor.
  Contact(Contact&&) = default;
  // move assignment.
  Contact& operator=(Contact&&) = default;
};

在这种情况下,将“五”设置为默认值实际上与省略它们是一样的。

在检查不写任何愚蠢内容时,我发现了以下链接:


@Caleth 当然。我同意。我假设 OP 展示了一个 MCVE,并且可能有其他原因使用指针来引用“Address”(例如老师要求这样做)。但是,我认为你的答案是第三个选项(为了总结主题)。 - Scheff's Cat
@Scheff 类的设计故意弱化了类的设计,因为这不是重点。展示boost序列化如何处理Address address和Address* address而不进行更改是老师的观点。当我使用老师的代码时,联系人没有像老师那样从lambda中返回。我实现了你和AndyG建议的方法,它像你预期的那样正常工作。不确定为什么在课程演示中会有不同的效果,因为我对C++相对较新,所以我把我的问题带到了这里。现在我确认我没有在lambda方面遗漏任何东西。 - AC Thompson
@ACThompson 我刚看了一下Boost.Serialization - Pointers and References,因为我无法想象你老师的例子应该是一个可行的例子。我了解到,如果存档被恢复,a不一定包含相同的地址。一个新对象被创建并将其地址分配给a。没有提到如何管理a的先前内容。(忽略?删除?)你的老师提供了一个新构造的对象,而你则提供了一个复制的对象。这就是区别所在。 - Scheff's Cat
@ACThompson 要记住我关于默认构造函数的写法,这可能解释了为什么你老师的例子可以工作,但你的不能。我同意,lambda在这里并没有起到积极的作用 - 一个独立的函数,或者只是这个位置的代码可能会导致相同的结果。 - Scheff's Cat
顺便提一下,上面链接中的文档还提到了“请注意,Boost.Serialization尚未更新为C++11。像std::shared_ptrstd::unique_ptr这样来自C++11标准库的智能指针目前不受Boost.Serialization支持。”奇怪的是,我找不到文档所依赖的Boost版本。(通常,您总是可以选择切换到当前版本或任何其他版本。)这使得它有点神秘... - Scheff's Cat
显示剩余3条评论

1
Contact中使用指针Address没有必要,因此不要使用。这也意味着编译器生成的复制构造函数可以替换clone
struct Contact
{
  string name;
  Address address;

  friend ostream& operator<<(ostream& os, const Contact& obj)
  {
    return os
      << "name: " << obj.name
      << " address: " << obj.address;
  }
private:
  friend class boost::serialization::access;

  template<class Ar> void serialize(Ar& ar, const unsigned int version)
  {
    ar & name;
    ar & address;
  }
};

int main()
{
  Contact john;
  john.name = "John Doe";
  john.address = Address{ "123 East Dr", "London", 123 };

  Contact jane = john;
  jane.name = "Jane";
  jane.address.street = "123B West Dr";

  cout << john << endl << jane << endl;

  getchar();
  return 0;
}

这是一个教授序列化的课程中的一部分,最初地址指针是一个结构体,后来改为指针,以展示boost序列化可以很好地处理指针。 - AC Thompson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接