【问题标题】:move or copy when passing arguments to the constructor and member functions将参数传递给构造函数和成员函数时移动或复制
【发布时间】:2018-06-27 14:03:38
【问题描述】:

以下是我的典型代码示例。 A 有很多看起来像这样的对象:

struct Config
{
    Config();
    Config(const std::string& cType, const std::string& nType); //additional variables omitted
    Config(Config&&) = default;
    Config& operator=(Config&&) = default;

    bool operator==(const Config& c) const;
    bool operator!=(const Config& c) const;

    void doSomething(const std::string& str);
    bool doAnotherThing(const MyOtherObject& obj);
    void doYetAnotherThing(int value1, unsigned long value2, const std::string& value3, MyEnums::Seasons value4, const std::vector<MySecondObject>& value5);

    std::string m_controllerType;
    std::string m_networkType;
    //...
};

//...

Config::Config(const std::string& cType, const std::string& nType) :
    m_controllerType(cType),
    m_networkType(nType)
{
}

我的动机和对该主题的一般理解:

  • 在构造函数和方法中使用 const 引用以避免在传递对象时重复复制。
  • 简单类型 - 按值传递;类和结构 - 通过 const 引用(或当我需要修改它们时的简单引用)传递
  • 强制编译器创建 default 移动构造函数和移动赋值,这样它就能够完成它的神奇魔法,同时它可以避免编写无聊的 ctor() : m_v1(std::move(v1)), m_v2(std::move(v2)), m_v3(std::move(v3)) {}
  • 如果表现不佳,请使用 libc 和原始指针,然后将其包装在课堂上并写注释。

我有一种强烈的感觉,即根据经验法则是有缺陷的,而且根本不正确。

看完cppreference、Scott Mayers、C++标准、Stroustrup等,我的感觉是:“是的,这里的每一个字我都懂,但还是没有任何意义”。我唯一能理解的就是当我的类包含不可复制的类型(如 std::mutexstd::unique_ptr)时,移动语义是有意义的。

我见过很多代码,人们通过值传递复杂的对象,比如大字符串、向量和自定义类——我相信这就是移动语义发生的地方,但是,再次,你如何将对象传递给函数通过移动?如果我是正确的,它会使对象处于“某种空状态”,使其无法使用。

所以,问题是: - 我如何正确地决定传递值和传递引用? - 我是否需要同时提供复制和移动构造函数? - 我需要显式编写移动和复制构造函数吗?我可以使用= default 吗?我的类主要是 POD 对象,因此不涉及复杂的登录。 - 调试时,我总是可以在我自己的类的构造函数中写std::cout &lt;&lt; "move\n";std::cout &lt;&lt; "copy\n";,但是我怎么知道stdlib 中的类会发生什么?

附:它可能看起来像是绝望的呐喊(确实如此),而不是一个有效的 SO 问题。我根本不知道如何比这更好地表述我的问题。

【问题讨论】:

    标签: c++ c++11 move move-semantics stdmove


    【解决方案1】:
    • 如果是原始类型,则按值传递。参考地点获胜。

    • 如果您不打算存储它的副本,请按值传递或const&amp;

    • 如果您想存储它的副本,并且移动非常便宜且复制成本适中,请传递

    • 如果某物的移动成本适中,并且是接收器参数,请考虑通过右值引用传递。用户将被强制std::move

    • 1234563

    The Rule of 0/3/5 描述了您应该如何处理复制分配/构造/销毁。理想情况下,您遵循 0 规则;除了资源管理类型之外,复制/移动/破坏都是=default。如果你想实现任何的复制/移动/破坏,你需要实现,=default=delete 每隔 5 个。

    如果您只为 setter 提供 1 个参数,请考虑同时编写 setter 的 &amp;&amp;const&amp; 版本。或者只是暴露底层对象。移动分配有时会重复使用存储,这很有效。

    放置看起来像这样:

    struct emplace_tag {};
    struct wrap_foo {
      template<class...Ts>
      wrap_foo(emplace_tag, Ts&&...ts):
        foo( std::forward<Ts>(ts)... )
      {}
      template<class T0, class...Ts>
      wrap_foo(emplace_tag, std::initializer_list<T0> il, Ts&&...ts):
        foo( il, std::forward<Ts>(ts)... )
      {}
    private:
      Foo foo;
    };
    

    还有许多其他方法可以允许“安顿”构造。参见标准容器中的emplace_backemplace(它们使用放置::new 来构造对象,转发传入的对象)。

    Emplace 构造甚至允许直接构造而无需使用具有正确设置的operator T() 对象进行移动。但这超出了这个问题的范围。

    【讨论】:

    • 我已经读到“按价值取值,当你想要一个副本时移动”不一定是最好的选择。 Further reading。但总的来说,这是可靠的建议。
    • @MaxLanghof 该链接适用于设置器,而不是构造器。构造函数没有预先存在的存储。这主要是我上面所说的(注意我说分配几乎总是应该只是=默认)。
    • “考虑为调用者提供一种方法,以在高度通用的代码中将构造嵌入到字段中,或者在您需要每一盎司性能的地方”您可以再解释一下吗?如果可能的话,举个例子。
    • @MaxLanghof 但是这个问题超出了施工范围,所以我现在已经解决了。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-11-19
    • 2021-04-24
    • 1970-01-01
    • 2012-09-11
    • 2013-03-27
    • 1970-01-01
    • 2020-02-25
    相关资源
    最近更新 更多