【问题标题】:Confusing interactions between constructors and copy/move semantics in C++混淆构造函数之间的交互和 C++ 中的复制/移动语义
【发布时间】:2021-08-08 21:56:17
【问题描述】:

对原始帖子进行了编辑,以提供更少的复制。

长期 C 程序员,自 2010 年以来第一次重新学习 C++,所以我对所有现代 C++11 及以后的东西都很陌生。我想我理解复制和移动构造函数背后的意图以及为什么要明确定义/默认/删除它们。但是,在我自己的练习程序中,我对不同构造函数类型之间的交互非常迷茫:

TerritoryUSA.hpp

#ifndef TERRITORYUSA_HPP
#define TERRITORYUSA_HPP

namespace TerritoryUSA {
    enum class Identifier {
        Alabama,
        Alaska,
        American_Samoa
    };

    class Territory {
    private:
        Identifier identifier;
    public:
        // Construct.
        Territory(Identifier identifier);
        //Territory(Territory& other) = delete;         // 1
        //Territory(Territory&& other) = delete;        // 2

        // Move.
        //Territory& operator=(Territory& other) = delete;  // 3
        //Territory& operator=(Territory&& other) = delete; // 4

        // Destruct.
        ~Territory() = default;

        // Methods.
        Identifier getIdentifier();
    };
}

#endif /* TERRITORYUSA_HPP */

TerritoryUSA.cpp

#include "TerritoryUSA.hpp"

using namespace TerritoryUSA;

// Construct.
Territory::Territory(Identifier identifier) : identifier{identifier} {}

// Methods.
Identifier Territory::getIdentifier() {
    return identifier;
}

ma​​in.cpp

#include <cstdio>
#include "TerritoryUSA.hpp"

using namespace TerritoryUSA;

int main() {
    Territory territories[]{
        Territory(Identifier::Alabama),
        Territory(Identifier::Alaska),
        Territory(Identifier::American_Samoa)
    };

    for (Territory territory : territories) {
        printf("%d\n", static_cast<int>(territory.getIdentifier()));
    }
}

输出

0
1
2
Program ended with exit code: 0

请注意,这是在 MacOS 上的 Xcode 中完成的,我也在苦苦思索,因为我在编写 C 时主要在 Linux 上使用 vim/make。我还没有弄清楚在哪里可以找到编译器版本然而,但这是一个全新的更新的 Macbook,新安装了 Xcode,所以它不会很旧。我所做的只是启动一个新的 Xcode“命令行工具”应用程序并使用右键菜单将 3 个文件放在项目目录中,因此构建系统应该是正确的(并且当标记的行被注释时它确实构建并运行成功)。

让我感到困惑的部分是,当我有选择地取消注释头文件中标记为 1-4 的行时,我得到了与 Territory 对象的构造相关的各种错误。以下是取消注释单行时得到的结果:

第 1 行:

main.cpp 的第 8-10 行(我尝试实例化 Territory 对象的地方)我收到这个红色错误:

No matching constructor for initialization of 'TerritoryUSA::Territory'

它还在 hpp 的灰色第 17 行(接受标识符参数的构造函数)中突出显示以下消息:

1. Candidate constructor not viable: no known conversion from 'TerritoryUSA::Territory' to 'TerritoryUSA::Identifier' for 1st argument

它还用这条消息以灰色突出显示未注释的第 1 行:

2. Candidate constructor not viable: expects an l-value for 1st argument

第 2 行:

main.cpp 的第 8-10 行(我尝试实例化 Territory 对象的地方)我收到这个红色错误:

Call to deleted constructor of 'TerritoryUSA::Territory'

它还用这条消息以灰色突出显示未注释的第 2 行:

1. 'Territory' has been explicitly marked deleted here

第 3 行:

没有错误

第 4 行:

main.cpp 的第 8-10 行(我尝试实例化 Territory 对象的地方)我收到这个红色错误:

Call to implicitly-deleted copy constructor of 'TerritoryUSA::Territory'

它还用这条消息以灰色突出显示未注释的第 4 行:

1. Copy constructor is implicitly deleted because 'Territory' has a user-declared move assignment operator

问题

对于第 1 行,为什么要尝试使用无参数构造函数和/或做出任何隐含假设?我不是为带有 1 个标识符参数的签名提供了一个完全有效的显式构造函数吗?我故意不希望用户在没有任何参数的情况下实例化 Territory 对象(坦率地说,我根本不希望他们实例化它们,而是一次一步实例化它们)。而且,删除复制构造函数与此有什么关系?我不想复制任何东西。

对于第 2 行,我知道我删除了默认构造函数,但我并没有尝试使用它,而是尝试使用带有标识符的构造函数。为什么我会被绊倒?此外,删除移动构造函数与此有什么关系?我不想移动任何东西。

对于第 4 行,如果我正在删除移动运算符,为什么会收到与已删除的复制构造函数相关的错误?我完全迷失了这一点,尤其是因为第 3 行没有任何错误。

【问题讨论】:

  • 尽量让你的示例 sn-p 最小化;枚举只需要几个成员就可以展示行为。
  • 无法复制。什么编译器?什么标准?您的 source (CPP) 文件是什么样的?
  • 你可以尝试在cpp文件中定义静态数组,而不是在标题中?如此处建议:stackoverflow.com/a/2117331/260313
  • 您在构造函数初始化器列表中使用花括号而不是圆括号构造枚举成员有什么特别的原因吗?换句话说,为什么用这个Territory(Identifier identifier) : identifier{identifier} {} 而不是这个? Territory(Identifier identifier) : identifier(identifier) {}
  • 我正在尝试更新我的示例以使其更简单一些,但请记住,我正在关注一本书,而且还很早,所以我并不熟悉如何正确地重构所有这些代码。要回答@Joe,我正在阅读的书是 Josh Lospinoso 的“C++ Crash Course: A Fast-Paced Introduction”,它建议始终使用支撑初始化。我忘记了确切的例子,但它提供了一些极端情况的例子,括号不能很好地工作或有点不直观。我正在努力养成良好的开始习惯,所以牙套似乎是要走的路。

标签: c++ copy-constructor move-constructor


【解决方案1】:

我相信我已经解决了我的问题,但不幸的是,它并没有真正说明我最初遇到问题的原因。但是,唉,我不是在搞清楚为什么新代码不能在旧编译器上编译,所以这个节目必须继续。

在 cmets 中进行了一些讨论并且有人能够毫无问题地构建和运行代码后,我在我的 Xcode 项目设置中进行了挖掘,发现它正在为 C++14 编译。我将其更改为 C++17,取消了所有 4 行的注释,并且大部分错误都消失了。

main.cpp 中我的 for 循环行还有 1 个错误:

Call to deleted constructor of 'TerritoryUSA::Territory'

它还用这条消息以灰色突出显示未注释的第 1 行:

1. 'Territory' has been explicitly marked deleted here

那行是复制构造函数,所以我猜测基于范围的 for 循环是使用复制构造函数实现的。我很快在该行中添加了一个引用修饰符,使其看起来像这样:

    for (Territory& territory : territories) {

瞧,它再次构建并运行。当我更经常地使用该语言时,我什至不认为 C++ 中存在基于范围的 for 循环,所以我不知道引用修饰符是否会在那里按预期工作,但似乎可以。非常漂亮,几乎就像没有复制的低级 Python。

【讨论】:

  • 那其实不是拷贝构造函数。至少不是你想要的那个。参数应该是const Territory&amp;,而不是Territory&amp;。但很高兴你解决了你的问题
  • 这将是我的下一个问题,尽管我不想使手头的问题过于复杂。对于何时需要 const ,我仍然有点模糊。我认为 const 只重要,因为它也适用于 r-values,但如果您提供移动构造函数,r-values 将使用该构造函数。啊,好吧,我并不是想在所有细节上打扰你,这是我需要更多研究和实践的东西。当然,在我的第一个示例项目中,我被编译器抛出了一个循环。
  • 这很简单。如果您正在调用的函数(在这种情况下是复制构造函数)应该不允许更改传入的参数,那么它应该是 const。绝对不应允许复制构造函数更改给定的内容。它复制它。由于您通过引用传入 Territory 对象,这意味着构造函数可以访问原始的底层对象。这就像传递一个指针。因此,如果您想向所有调用者发出信号表明它不会被更改,请将其标记为const Territory&amp;
  • 现在,如果 Territory 是一个非常微不足道的类,它的语义允许复制/分配(我认为它没有)。然后你可能有一个复制构造函数,它只接受Territory 的参数(即通过值传递隐式复制),而不是const Territory&amp; 但你显然试图禁止这样做,所以const Territory&amp; 它是。
  • 我应该补充一点,我早在 1990 年代就开始使用 C++,早在诸如 R 值引用之类的东西出现之前。这就是为什么我询问构造函数初始化器列表的花括号而不是括号的原因。我对“最佳实践”的想法偶尔会被语言改进所克服。 (所以我需要阅读一下为什么这种做法更好......)但是我在上面的两个 cmets 中写的所有内容在技术上仍然是正确的
猜你喜欢
  • 2018-08-27
  • 1970-01-01
  • 1970-01-01
  • 2013-11-05
  • 2015-02-14
  • 1970-01-01
  • 1970-01-01
  • 2018-04-01
  • 1970-01-01
相关资源
最近更新 更多