移动语义:执行 std::move 时不执行构造函数和赋值

Move Semantics: Constructor and Assignment are not executed when performed std::move

提问人:Kevinkun 提问时间:7/23/2021 更新时间:7/23/2021 访问量:88

问:

#include <iostream>
#include <utility>
#include <vector>

class Node
{
public:
    int data;
    Node* prev;
    Node* next;
};

class Doublyll
{
private:
    Node* head;
    Node* tail;
public:
    Doublyll();
    Doublyll(std::vector<int> V);
    Doublyll(const Doublyll& source);
    Doublyll(Doublyll&& src) noexcept;
    Doublyll& operator=(const Doublyll& rhs);
    Doublyll& operator=(Doublyll&& src) noexcept;
    ~Doublyll();

    friend std::ostream& operator<<(std::ostream& os, const Doublyll& src);

    void Concatenate(Doublyll&& l2);
};

// Default Constructor will SET head and tail to nullptr
Doublyll::Doublyll()
    : head(nullptr), tail(nullptr)
{
}

// Explicit Constructor using vector
Doublyll::Doublyll(std::vector<int> V)
    : head(nullptr), tail(nullptr)
{
    Node** p = &head;

    for (auto& value : V)
    {
        Node* t = new Node;
        t->data = value;

        if (head == nullptr)
            t->prev = nullptr;
        else
            t->prev = tail;
        t->next = nullptr;

        *p = t;
        p = &(t->next);

        tail = t;
    }
}

// Copy Constructor
Doublyll::Doublyll(const Doublyll& source)
    : head(nullptr), tail(nullptr)
{
    std::cout << "Copy Construcor called!\n";

    Node** p = &head;

    // Iterate through all Node in source linked list, copying it to new object
    for (Node* tmp = source.head; tmp != NULL; tmp = tmp->next)
    {
        Node* t = new Node;
        t->data = tmp->data;
        if (head == nullptr)
            t->prev = nullptr;
        else
            t->prev = tail;
        t->next = nullptr;

        *p = t;
        p = &(t->next);

        tail = t;
    }
}

// Move Constructor
Doublyll::Doublyll(Doublyll&& src) noexcept
    : head(std::exchange(src.head, nullptr)), tail(std::exchange(src.tail, nullptr))
{
    std::cout << "Move Constructor called!\n";
}

// Copy Assignment Operator
Doublyll& Doublyll::operator=(const Doublyll& rhs)
{
    std::cout << "Copy Assignment Operator called!\n";

    // Check self assignment
    if (this != &rhs)
    {
        Doublyll tmp(rhs);
        std::swap(tmp.head, head);
        std::swap(tmp.tail, tail);
    }
    return *this;
}

// Move Assignment
Doublyll& Doublyll::operator=(Doublyll&& src) noexcept
{
    std::cout << "Move Assignment called!\n";

    if (this != &src)
    {
        std::swap(head, src.head);
        std::swap(tail, src.tail);
    }
    return *this;
}

// Destructor
Doublyll::~Doublyll()
{
    std::cout << "Desctructor called @ address " << &head << std::endl;

    Node* p = head;
    Node* tmp;

    while (p != nullptr)
    {
        tmp = p;
        p = p->next;
        delete tmp;
    }
}

// Display using Overloading << Operator
std::ostream& operator<<(std::ostream& os, const Doublyll& src)
{
    Node* tmp = src.head;

    if (tmp == NULL)
        std::cout << "(EMPTY)\n";
    else
    {
        for (; tmp != nullptr; tmp = tmp->next)
            std::cout << tmp->data << " ";
        std::cout << std::endl;
    }

    return os;
}

void Doublyll::Concatenate(Doublyll&& l2)
{
    // Since we have tail, we can connect it by using it
    tail->next = l2.head;
    l2.head->prev = tail;

    // Move first Node's tail to last Node of second linked list
    tail = l2.tail;
    
    // Make l2 as NULL
    /*l2.head = l2.tail = nullptr;*/
}

int main()
{
    // Create an Vector
    std::vector<int> v1 = { 1, 3, 5, 7, 9, 11 };
    std::vector<int> v2 = { 2, 4, 6, 8 };

    // Create object and linked list
    Doublyll l1(v1);
    Doublyll l2(v2);

    // Display linked list
    std::cout << l1;
    std::cout << l2;

    // Concatenate 2 linked list
    l1.Concatenate(std::move(l2));

    // Display agaian after concatenate, l1 should connect with l2. and l2 should be EMPTY
    std::cout << l1;
    std::cout << l2;

    std::cin.get();
}

在此代码中,我尝试连接使用 Vector 创建的 2 个链表

在 main() 中,我使用 std::move 调用,因为在最后,在 l1 与 l2 连接后,我希望在调用 Destructor 时变为 NULL 以防止双重释放。但是,似乎我创建的移动构造函数或移动赋值都没有执行。l1.Concatenate(std::move(l2));l2

事情是这样的:

  1. 我知道我不需要使用 std::move(l2),我可以简单地传递 引用 .它奏效了。但是我想练习使用移动构造函数和赋值。l1.Concatenate(l2)void Doublyll::Concatenate(Doublyll& l2)
  2. 你可以看到我评论的这段代码:我已经手动完成了它,将其设置为 NULL。它将完美地工作,不会导致双重免费。但是,如果我执行 std::move(l2),我不需要手动完成,对吧?void Doublyll::Concatenate(Doublyll&& l2)/*l2.head = l2.tail = nullptr;*/l2
  3. 如果我像这样将旧对象移动到新对象,我的移动构造函数和移动分配实际上正在工作: 像这样Doublyll l3(std::move(l2));l2 = std::move(l1);

那么,为什么当我尝试这样调用它来连接两个链表时,您认为它不起作用?我以为这就像在使 l2 无法访问 (NULL) 之前将 l2 移动到 l1。l1.Concatenate(std::move(l2));

std::move 不是那样工作的吗?或者也许我的代码有问题,或者还有另一种方法仍在使用 std::move 来执行这样的功能?也许你也可以向我解释一下这个动作的语义。谢谢。

C++ 链表 移动语义

评论

1赞 JaMiT 7/23/2021
这看起来像是很多代码来演示涉及 .你有没有试过把它简化成更简单的东西?专注于线条,只保留展示你的观点所需的内容。试着更抽象地思考。不要担心链表和相关功能;重点关注指示正在调用(和未调用)的诊断消息。你有一个类,叫它,你想移动。类除了跟踪成员函数调用外没有其他功能。你能用 ?std::movestd::moveAAA
1赞 JaMiT 7/23/2021
我还建议查看如何提问,特别是关于在代码之前提出问题的部分。强迫自己用语言描述你的处境往往会带来更多的理解,即使没有人回答你的问题。
1赞 JaMiT 7/23/2021
“也许你也可以向我解释一下这个动作的语义。”-- 如果这是你真正的问题(记住:每个问题一个问题),那么你可能在重复什么是移动语义?另一个潜在的重复项:什么是 std::move(),什么时候应该使用它?这些都回答了你的问题吗?
0赞 Enlico 7/23/2021
看看 std::move 和 std::forward 有什么区别。这应该澄清很多是什么以及它的用途。std::move

答:

0赞 Daniel McLaury 7/23/2021 #1

代码不起作用,因为您注释掉了使其工作的部分,即

l2.head = l2.tail = nullptr;

您看不到正在使用的移动构造函数或移动赋值运算符,因为您尚未编写任何使用它们的代码。你以为这里有什么东西会叫他们吗?

我不确定你的问题是什么,但我猜你对什么是移动语义有一个根本性的误解。唯一能做的就是指示允许将某些东西传递给以 a 作为参数的函数。当你编写一个函数时,你的责任是将你传递的东西中的胆量移出,并让它处于一种状态,它所能做的就是在不引起任何问题的情况下进行破坏。这正是您在手动将头部指针设置为 时所做的。std::moveT&&T&&nullptr

评论

0赞 Kevinkun 7/23/2021
我认为你没有正确阅读我上面的陈述。我知道代码:已起作用。但是我注释掉了,因为我认为如果使用移动语义,它会自动将对象设置为.所以我不需要手动执行此操作。l2.head = l2.tail = nullptr;l2NULL
0赞 Kevinkun 7/23/2021
而且我也知道是传递给一个带有参数的函数。看看我的函数:.我还创建了移动构造函数和移动赋值来处理它。std::move&&void Doublyll::Concatenate(Doublyll&& l2)
0赞 Daniel McLaury 7/23/2021
“而且我也知道 std::move 是传递给一个带有 && 作为参数的函数。”是的,但你似乎不明白这就是它的全部作用,因为你似乎认为写作会以某种方式调用移动构造函数或移动赋值运算符。std::move
0赞 Kevinkun 7/23/2021
那么你的意思是 std::move 不会调用移动构造函数或赋值?因为当你用它把一个对象移动到一个新的或现有的对象时,它当然会调用移动构造函数/赋值。例如:LinkedList l2(std::move(l1)) 我们将 l1 移动到 l2。那么你的观点是什么?
0赞 Daniel McLaury 7/23/2021
如果 (1) 执行调用构造函数的操作,则将调用移动构造函数;(2) 存在移动构造函数;(3) 构造函数参数是右值引用。同样,如果 (1) 您执行执行分配的操作,则将调用移动分配运算符;(2)存在移动分配操作员;(3)您要分配的东西是右值引用。