这样的写作业有什么问题?

What are problems with writing assignment like this?

提问人:Joker_vD 提问时间:2/14/2014 最后编辑:Bernhard BarkerJoker_vD 更新时间:2/14/2014 访问量:138

问:

前几天,我和我的一个朋友就对象分配和构造进行了一次对话,他指出,对象的分配(在语义上)等同于摧毁它,然后从(在同一个地方)重新构造它。a = bab

但是,当然,没有人(我认为)像这样编写赋值运算符:

class A {
    A& operator=(const A& rhs) {
        this->~A();
        this->A(rhs);
        return *this;
    }

    A& operator=(A&& rhs) {
        this->~A();
        this->A(std::move(rhs));
        return *this;
    }

    // etc.
};

[注意:我不知道如何在现有对象上手动调用构造函数/析构函数(我从来没有这样做过!),所以它们的调用可能没有形式意义,但我想你可以看到这个想法。

这种方法有什么问题?我想必须有一个主要的表演者,但名单越大越好。

C++ 造函数 析构函数 赋值运算符

评论

0赞 Joker_vD 2/14/2014
@MatsPetersson 是的,这是一个问题。好吧,让我们假装有守卫。或者,该标准要求实现始终检查自分配本身并始终省略它。别的东西?if (reinterpret_cast<void*>(this) == reinterpret_cast<void*>(&rhs)) { return (*this); }
0赞 CouchDeveloper 2/14/2014
赋值运算符以您实现的方式执行。通常,不需要“销毁”一个对象,然后“重新分配”它,只是为了实现一个赋值。以 std::vector 为例:赋值不会执行与析构函数等效的操作(释放原始分配的存储),而只是将新元素复制或移动到现有存储中(如果它适合它)。同样的情况也发生在元素本身上。

答:

1赞 Wintermute 2/14/2014 #1

首先,只有当对象是使用 overload 构造的时,才需要手动调用析构函数,并具有一些期望,例如使用重载。operator new()std::nothrow

你要理解的是复制构造和赋值运算符之间的区别:当从现有对象创建新对象时,将调用复制构造函数,作为现有对象的副本。当从另一个现有对象为已初始化的对象分配新值时,将调用赋值运算符。

总而言之,您提供的赋值运算符示例没有意义 - 它必须具有不同的语义。

如果您还有其他问题,请发表评论。

0赞 Marco A. 2/14/2014 #2

首先,直接调用复制构造函数是不合法的(至少在兼容 C++ 的编译器中是这样。VS2012 允许这样做),因此不允许以下情况:

  // assignment operator
  A& operator=(const A& rhs) {
  this->~A();
  this->A::A(rhs); <---  Invalid use

此时,您可以依赖编译器优化(请参阅复制省略和 RVO)或将其分配到堆上。

如果您尝试执行上述操作,可能会出现许多问题:

1) 您可能会在复制构造函数的表达式中抛出异常

在这种情况下,您将拥有

  // assignment operator
  A& operator=(const A& rhs) {
    cout << "copy assignment called" << endl;
    this->~A();
    A newObj(rhs); // Can throw and A is in invalid state!
    return newObj;
  }

为了安全起见,您应该使用复制和交换的习惯用语:

set& set::operator=(set const& source)
{
    /* You actually don't need this. But if creating a copy is expensive then feel free */
    if (&source == this)
        return;

    /*
     * This line is invoking the copy constructor.
     * You are copying 'source' into a temporary object not the current one.
     * But the use of the swap() immediately after the copy makes it logically
     * equivalent.
     */
    set tmp(source);
    this->swap(tmp);

    return *this;
}

void swap(set& dst) throw ()
{
    // swap member of this with members of dst
}

2) 动态分配的内存可能有问题

如果 A 的两个实例共享一个指针,则在能够释放它之前,您可能有一个悬空的指针

  a = a; // easiest case

  ...

  // assignment operator
  A& operator=(const A& rhs) {
  this->~A(); <-- Freeing dynamically allocated memory
  this->A::A(rhs); <---  Getting a pointer to nowhere

3)正如Emilio所指出的,如果类是多态的,你将无法重新实例化该子类(除非你用类似CRTP的技术以某种方式欺骗它)

4)最后,赋值和复制构造是两种不同的操作。如果 A 包含重新获取成本高昂的资源,您可能会发现自己陷入了很多麻烦。

评论

0赞 Joker_vD 2/14/2014
第一段无关紧要 - 不会创建本地对象。是的,赋值的方式可能很慢:这基本上是“数学家和水壶”的笑话。复制和交换基本上做同样的事情,只是顺序略有不同:构造 from ,交换 和 的内脏,然后销毁(因此,旧的内容)。但可悲的是,它需要一个“空状态”。tmpbtmpatmpa
0赞 Marco A. 2/14/2014
让我们看看我是否可以编辑我的答案并提供一些(我想)可能感兴趣的具体案例
2赞 Emilio Garavaglia 2/14/2014 #3

这里有一个误用的结构:

class A {
    A& operator=(const A& rhs) {
        if(&a==this) return *this; 
        this->~A();
        new(this) A(rhs);
        return *this;
    }

    A& operator=(A&& rhs) {
        if(&a==this) return *this; 
        this->~A();
        new(this) A(std::move(rhs));
        return *this;
    }

    // etc.
};

这是对就地 ctor/dtor 语义的正确尊重,这就是在缓冲区中破坏和构造元素的作用,所以这必须是正确的,对吧?std::allocator

井。。。不正确:这一切都是关于 A 实际上包含的内容以及 A ctor 实际做什么。

如果 A 只包含基本类型并且不拥有资源,那很好,它可以工作。这不是惯用的,但是正确的。

如果 A 包含一些其他资源,需要好好获取、管理和释放......你可能会遇到麻烦。如果 A 是多态的,你也是(如果 ~A 是虚拟的,你破坏整个对象,但随后你只重建 A 子对象)。

问题在于,获取资源的构造函数可能会失败,而在构造中失败并抛出的对象不能被销毁,因为它从未被“构造”过。

但是,如果你在“分配”,你就不是在“创建”,如果就地 ctor 失败,你的对象将存在(因为它预先存在于它自己的范围内),但处于无法通过进一步销毁来管理的状态:想想看

{
  A a,b;
  a = b;
}

在 b 和 a 将被销毁,但如果 A(const A&) 在 a=b 中失败,并且 a 在 中不存在,但会在 被不正确地销毁,这将立即跳转到。}throwA::Aa}throw

一种更惯用的方法是拥有

class A
{
   void swap(A& s) noexcept
   { /* exchanging resources between existing objects should never fail: you just swap pointers */ }
public:
   A() noexcept { /* creates an object in a "null" recognizable state */ }
   A(const A& s) { /* creates a copy: may fail! */ }
   A(A&& s) noexcept { /*make it as null and... */ swap(s); } // if `s` is temporary will caryy old resource deletionon, and we keep it's own resource going
   A& operator=(A s) noexcept { swap(s); return *this; }
   ~A() { /* handle resource deletion, if any */ }
};

现在

  a=b

将创建一个副本作为 中的参数(通过 )。 如果这失败了,将不存在并且仍然有效(具有自己的旧值),因此在范围退出时将像往常一样被销毁。 如果复制成功,则复制的资源和实际的资源将被交换,当死亡时,旧的资源将被释放。bsoperator=A::A(const A&)sabas}

作者:converse

a = std::move(b)

将作为临时的,通过 A(A&&) 构造的参数,因此 b 将与 s 交换(并变为 null),而不是 s 将与 a 交换。最后,将销毁旧资源,接收旧资源,b 将处于空状态(因此当其范围结束时,它可以平静地死亡)bssaab

“使 A 为 null”的问题必须在 和 中实现。 这可以通过帮助程序成员(一个 ,就像 一样)或通过指定成员初始值设定项,或者通过定义成员的默认初始化值(一次)A()A(A&&)initswap

评论

0赞 Joker_vD 2/14/2014
最后,对“为什么它不起作用”的实际解释。谢谢!好吧,我想现在我将回到编写自定义 s 和默认构造函数:(swap
0赞 MSalters 2/14/2014
+1 用于将复制、移动和交换识别为更原始的操作。但是,假设必须保留为“null”是错误的。根本没有这样的要求。任何状态都可以,只要对象可以被销毁。特别是,可以保持原样。A::A(A&& rhs)rhsrhs
0赞 Emilio Garavaglia 2/14/2014
@MSalters:这完全取决于“状态”是什么:如果你正在转移资源,移动后的 b 状态一定不能再保留资源。称其为 null 或任何只是一个约定。重要的是析构函数知道该怎么做。我同意。
0赞 MSalters 2/14/2014
@EmilioGaravaglia:这里没有太多空间来解释“视情况而定”,语义完全由C++11定义。而且他们不强制要求断认.此外,他们不假设有一个 aka 状态。bAnulldefault-constructed
0赞 Emilio Garavaglia 2/14/2014
@MSalters:如果 b 拥有某物,而你把那样东西(不是 b,而是它拥有的东西)移到了 a,那么 b 就不能再拥有它了。这不是C++规范这么说,而是“拥有”的简单英语含义。因此,b 必须有一个传统上说“不拥有”的值,这样 b dtor 就不会释放它不拥有的东西。我简直无法理解你怎么能让 b 原封不动地做到这一点。C++ 规范不需要它,但你正在做的事情的语义需要它。程序必须执行的操作不是由 C++ 指定的,而是由您的承诺指定的。