【问题标题】:How to write move constructor to handle uninitialized move?如何编写移动构造函数来处理未初始化的移动?
【发布时间】:2019-10-05 19:38:13
【问题描述】:

我的 C++ 代码中有一个类,它有自己的移动构造函数。此处显示了简化版本:

class myClass {
    //In this example, myClass must manually manage allocating
    //and freeing a memory buffer.
    char *mem;

    //...
    //Regular constructor, copy constructor, etc
    //...

    myClass(myClass &&other) {
        //Swap our memory pointer with other's memory pointer
        char *tmp = other.mem;
        other.mem = mem;
        mem = tmp;
    } 

    //...
    //Destructor, other member functions, etc.
    //...
}

在正常情况下,这可以正常工作。但是,最近我需要制作这些对象的向量:

vector<myClass> v;
v.reserve(10); //Make space, but do not construct
v.push_back(myClass()); //Problem!

在遇到段错误并逐步使用 gdb 后,我最终发现了应该很明显的事情:如果您尝试从右值引用构造对象,这可能会导致在未初始化的内存上使用移动构造函数时间>。

当您可能将垃圾交换到other 类中时,您应该如何编写移动构造函数?有什么方法可以检测到这一点吗?

【问题讨论】:

  • 你需要内存缓冲区,还是真的需要std::string
  • 为了在论坛上提问,我简化了这个例子。在极少数情况下,您确实想管理自己的记忆。无论如何,std::string 是如何解决这个问题的?
  • 我认为您需要向我们展示常规的无参数构造函数。大概它初始化mem,如果只是nullptr? (另外,在您的复制构造函数中没有理由使用other.mem = mem;更好的是free (mem); mem = other.mem; other.mem = nullptr;
  • 构造函数总是被应用于未初始化的内存。所以不要交换指针。只需将其复制到正在构造的对象并将移动的指针设置为 0。
  • @Mahkoe 在初始化内存上调用构造函数是未定义的行为。您正在访问未初始化指针 (mem) 的值。

标签: c++ stdvector move-semantics


【解决方案1】:

当您可能将垃圾交换到另一个类时,您应该如何编写移动构造函数?有什么方法可以检测到这一点吗?

在分配另一个值[basic.indet]/1 之前,未初始化的对象拥有一个不确定的值。基本上不允许你对持有不确定值的对象做任何事情,除非给它分配一个适当的值[basic.indet]/2。由于您甚至不允许查看对象所拥有的值,除非它已被初始化或赋值,因此不可能仅通过查看对象本身来检测对象是否已初始化(因为您甚至不允许看)。因此,严格来说,您实际上不仅仅是“将垃圾值交换到另一个类中”,而是在调用未定义的行为。被交换的垃圾正是这种未定义行为的典型表现方式。

问题的解决方法很简单:确保你的指针总是被初始化为一个有效值,例如nullptr

class myClass {
    //In this example, myClass must manually manage allocating
    //and freeing a memory buffer.
    char *mem = nullptr;

    //...
    //Regular constructor, copy constructor, etc
    //...

    myClass(myClass &&other) {
        //Swap our memory pointer with other's memory pointer
        char *tmp = other.mem;
        other.mem = mem;
        mem = tmp;
    } 

    //...
    //Destructor, other member functions, etc.
    //...
}

与其自己实现移动构造函数,不如考虑,例如,只使用std::unique_ptr 类型的成员并简单地依赖隐式定义的移动构造函数。例如:

class myClass
{
    std::unique_ptr<char[]> mem;

    // regular constructor, copy constructor, etc.

    myClass(myClass&&) = default;

    // other member functions, etc.
};

【讨论】:

  • 使用 mem-initializer 和 std::exchange() 也很有趣。或者也许是复制和交换。当然,最好遵循您所展示的零规则,尽管为什么您记下要删除的复制ctor...
  • @Deduplicator 是的,我不确定我在想什么复制构造函数,感谢您指出这一点……已修复……我想是时候睡觉了……
  • 为什么还要费心交换呢?只需在移动构造函数的成员初始化器中初始化成员,并在构造函数中将其他对象的指针设为空。
【解决方案2】:

不要在构造函数中交换指针。这不是您编写移动构造函数的方式。当两个对象都处于活动状态时,交换用于移动分配。

构造函数用于初始化对象。因此,它们开始的内存始终处于“未初始化”状态。所以除非你初始化一个成员(或者它有一个为你初始化它的默认构造函数),否则成员的值将开始未初始化。

处理这个问题的正确方法是复制成员初始化器中的指针,然后将另一个指针设为空。

myClass(myClass &&other) : mem(other.mem) {
    other.mem = nullptr;
}

或者,对于 C++14(以及带有 constexpr 版本的 C++20),您可以 exchange the value

myClass(myClass &&other)
  : mem(std::exchange(other.mem, nullptr))
{}

【讨论】:

  • 做同样事情的更紧凑的方法(@Deduplicator 也建议)是使用std::exchange(other.mem, nullptr) 来初始化mem
  • @MichaelKenzel:如果您需要构造函数为constexpr,则不需要。不过,C++20 解决了这个问题。
  • 如果 other.mem 在 OP 的场景中未初始化,这仍然会导致未定义的行为
  • @M.M: other.mem 不能未初始化,因为这意味着myClass损坏。 OP从未说过他们从未在“常规构造函数,复制构造函数”等中初始化mem。事实上,作为与其他构造函数不同的构造,“常规构造函数”的存在告诉我们,它要么是默认构造函数,要么是阻止自动生成的默认构造函数的构造函数。简而言之,OP 从来没有声称other.mem 未初始化,如果是,那是代码的错误没有显示给我们。
  • @M.M:崩溃来自这样一个事实,即 OP 将未初始化的值交换为已初始化的值(other.mem 的初始值),然后销毁包含未初始化值的 prvalue。事实上,OP 用“你正在将垃圾交换 other 类”这句话清楚地说明了这一点。 OP 正在考虑的“未初始化”类是正在构建的类,而不是 other
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-11-01
  • 2014-09-07
  • 2013-11-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多