【问题标题】:Why can't I use a copy constructor inside of my assignment operator?为什么我不能在赋值运算符中使用复制构造函数?
【发布时间】:2021-04-30 02:46:42
【问题描述】:

标题恰到好处。

    // copy constructor
IntList::IntList(const IntList& source){
    Node* currentNode = source.first;
    first = nullptr;
    while(currentNode){
        append(currentNode->info);
        currentNode = currentNode->next;
    }
}

//Assignment operator should copy the list from the source
//to this list, deleting/replacing any existing nodes
IntList& IntList::operator=(const IntList& source){
    this -> ~IntList();
    this = new IntList(source);
    return *this;
}

错误: intlist.cpp:26:30:错误:需要左值作为赋值的左操作数 this = new IntList(source);

析构函数正确地删除链表中的每个节点。从技术上讲,“this”是使用赋值运算符传入的任何链表。那不就当成可以设置的对象了吗?

【问题讨论】:

  • this -> ~IntList(); -- 这是错误的。这不应该在你的课堂上完成。只需简单的copy/swap 成员即可,无需致电new
  • 在纯粹的机械层面上,this 是不可修改的。 this = whatever; 是非法的。

标签: c++


【解决方案1】:

简短的回答是您不能分配this。 (this 是指向当前对象的指针,它不能被“重定位”,它是一个用于所有目的的“const”指针)。

你的意思是这样的:

    new(this) IntList(source);

这叫“放置”new

但您无需了解这一点,因为无论如何这一切都很糟糕。继续阅读...


你的设计模式是一种陈旧而糟糕的设计模式,将赋值实现为破坏+构造。

如今,除非有更好的捷径(取决于您的具体设计),否则我们会实施交换(也很有用),然后 operator= from copy(construct)+swap。

(除了对析构函数的任何显式调用都很难闻。)

IntList& IntList::operator=(IntList const& source){
    IntList tmp(source)
    this->swap(tmp);
    return *this;
}

重点不仅是更优雅的代码,最终的副作用是默认情况下您正在编写异常安全的代码。 (想想如果无法在原始代码中构造新对象会发生什么:*this 对象处于无效的“已破坏”状态。)

查看系列讲座:https://www.youtube.com/watch?v=W7fIy_54y-w


这现在真的很先进,而且变得很奇怪(正如与@PaulMcKenzie 所讨论的那样)。 事实证明(我认为只有 C++14 中的一些怪癖),您可以省略临时并将其转移到按值传递的参数本身:

IntList& IntList::operator=(IntList source) noexcept{
    return swap(source);
}

虽然一开始可能很模糊。

  1. 代码更少
  2. 它是noexcept:它不会失败!。操作 call 可能会失败,但不会在函数内
  3. 如果有移动构造函数,这将提供移动赋值,因此您无需编写单独的移动赋值(甚至更少的代码)。

(顺便说一句,我认为这说明了为什么成员交换应该返回*this

这引发了一个有趣的问题:如果我有复制构造函数交换,编译器不应该为我自动实现operator= 吗? (不是旧的 C 运算符 = member-by-member,而是一个知道所实现类的语义的运算符,如图所示)。 IntList& operator=(IntList) = default;


最后,不要只见树木不见森林。

你想在不编写新代码的情况下实现赋值,假设你的复制构造函数很好这就是你所拥有的一切

然而这仍然不是最优的!

对于容器的复制构造通常涉及 a)(无条件)分配和 b)一系列“未初始化的副本”(最终是一种放置新的形式)。 a) 和 b) 分别比 A) 不分配和 B) assignment 序列慢。

这意味着,如果出于某种原因您知道不需要分配(例如,因为源已经“适合”在您分配的内存中),并且您可以通过元素(比逐个元素的新位置更快)您可能处于良好状态,可以通过编写考虑所有这些的自定义operator= 来获得更优化的解决方案。

【讨论】:

  • 谢谢你...是否仍然需要在旧 IntList 上调用析构函数以防万一它存储在堆中...即: IntList listA = new IntList(参数); IntList listB(参数);列表A =列表B;如果你不在'this'上调用析构函数,如果你从堆中传递任何东西,你不会有内存泄漏吗?
  • @JustinKasowski IntList listA = new IntList(parameters); IntList listB(parameters); listA = listB;。这是完全错误的。您正在将对象分配给指针。您应该了解指针和动态内存分配的工作原理,最好阅读一些good book about C++ for beginners
  • @JustinKasowski -- 如果你看一下代码,哪里会出现内存泄漏?没有调用new,一切都是基于价值的。 tmp 的析构函数在tmp 自动超出范围时调用,delete[]-ing 旧内存。这就是析构函数的目的。
  • 被重载的原始 IntList 可能是用“new”创建的。 edit 我现在明白了,你用 tmp 交换你传入的任何 IntList,然后 tmp 被清除。不知道为什么我对此一无所知
  • @JustinKasowski -- 那么使用new 创建它是编码员的“错误”。它与IntList 本身的内部完全无关,这是您的问题和给出的答案所涉及的内容。
【解决方案2】:

如果IntList 的复制构造函数和析构函数工作正常,实现赋值运算符的最简单方法是使用copy/swap idiom

#include <algorithm>
//...
IntList& IntList::operator=(const IntList& source)
{
    if (&source != this)
    {
       IntList temp(source);
       std::swap(temp.first, first);
       // add swaps for each member
       //...
    }  // <-- This is where the old contents will be destroyed, 
       // when `temp` goes out of scope.  No need for this->~IntList();
    return *this;
}

在你的代码中,这样做:

this->~IntList();

不是必需的,而且可能是错误的。此类代码保留用于placement-new,您的代码没有显示任何placement-new 用法。

【讨论】:

  • 条件检查在语义上是不必要的,并且是可疑的优化。除非你喜欢经常写my_list = my_list;,或者你的算法被设计成做不必要的间接自赋值。
  • 我知道自检是没有必要的。但我不知道创建 IntList 是否昂贵,因为 OP 没有发布其详细信息。进行此检查没有任何害处。
  • 是的,害处是每次都做一次检查,因为每个蓝月亮你都可能做一个考虑不周的自我分配。无论IntList 的构建/复制成本是否昂贵,我都坚持这一点。
  • 那么,如果这是您关心的问题,那么赋值运算符应该是:IntList&amp; IntList::operator=(IntList source),没有自检。
  • 是的,我同意。在这种情况下也可以是noexcept ;)
【解决方案3】:

一个常见的错误是从复制构造函数中调用赋值运算符。以 Array 类为例。里面的文案你可能觉得写起来很简单

【讨论】:

  • 这应该是评论,而不是答案。
猜你喜欢
  • 1970-01-01
  • 2011-05-21
  • 2014-03-04
  • 2011-07-19
  • 1970-01-01
  • 1970-01-01
  • 2011-02-08
  • 1970-01-01
相关资源
最近更新 更多