2

当您打算存储对象时,我对“按值接受”的“规则”感到满意,而当您只需要访问它时,通过 const 引用接受。这样,您(类的编写者)不会选择是复制类变量的用户还是移动它,他们会选择。但在使用过程中,我越来越不确定该建议是否合理。move在成本操作中不是统一的...

struct Thing
{
    std::array<int, 10000> m_BigArray;
};

class Doer
{
    public:
        Doer(Thing thing) : m_Thing(std::move(thing)) {}

    private:
        Thing m_Thing;
};

int main()
{
    Thing thing;
    Doer doer1(std::move(thing)); // user can decide to move 'thing'
    // or
    Doer doer2(thing); // user can decide to copy 'thing'
}

在上面的示例中,移动与复制一样昂贵。因此,您最终会移动它(有效地复制它)两次,而不是获取一个 const 引用并复制一次对象。用户在您的论点中执行一次,您在您的成员中再次执行。

即使移动比复制便宜得多,这种情况也会进一步恶化(假设移动的成本低于某些未知成本,但比复制便宜):

struct A
{
    A(Thing thing) : m_Thing(std::move(thing)) {}
    Thing m_Thing;
};

struct B : A
{
    B(Thing thing) : A(std::move(thing)) {}
};

struct C : B
{
    C(Thing thing) : B(std::move(thing)) {}
};

struct D : C
{
    D(Thing thing) : C(std::move(thing)) {}
};

在这里,您要么以 1 个副本和 4 个动作结束,要么以 5 个动作结束。如果所有构造函数都接受一个 const 引用,那么它只会是 1 个副本。现在我必须权衡什么是昂贵的移动,1 副本或 5 移动。

处理这些情况的最佳建议是什么?

4

5 回答 5

4

处理这些情况的最佳建议是什么?

我最好的建议是做你正在做的事情:自己思考。不要相信你听到的一切。措施。

下面我获取了您的代码并使用打印语句对其进行了检测。我还添加了第三种情况:从纯右值初始化:

测试尝试两种方式:

  1. 按值传递。
  2. 通过const&and时重载&&

代码:

#include <utility>
#include <iostream>

struct Thing
{
    Thing() = default;
    Thing(const Thing&) {std::cout << "Thing(const Thing&)\n";}
    Thing& operator=(const Thing&) {std::cout << "operator=(const Thing&)\n";
                                    return *this;}
    Thing(Thing&&) {std::cout << "Thing(Thing&&)\n";}
    Thing& operator=(Thing&&) {std::cout << "operator=(Thing&&)\n";
                                    return *this;}
};

class Doer
{
    public:
#if PROCESS == 1
        Doer(Thing thing) : m_Thing(std::move(thing)) {}
#elif PROCESS == 2
        Doer(const Thing& thing) : m_Thing(thing) {}
        Doer(Thing&& thing) : m_Thing(std::move(thing)) {}
#endif

    private:
        Thing m_Thing;
};

Thing
make_thing()
{
    return Thing();
}

int main()
{
    Thing thing;
    std::cout << "lvalue\n";
    Doer doer1(thing); // user can decide to copy 'thing'
    std::cout << "\nxvalue\n";
    Doer doer2(std::move(thing)); // user can decide to move 'thing'
    std::cout << "\nprvalue\n";
    Doer doer3(make_thing()); // user can decide to use factor function
}

对我来说,当我使用 -DPROCESS=1 编译时,我得到:

lvalue
Thing(const Thing&)
Thing(Thing&&)

xvalue
Thing(Thing&&)
Thing(Thing&&)

prvalue
Thing(Thing&&)

并使用 -DPROCESS=2:

lvalue
Thing(const Thing&)

xvalue
Thing(Thing&&)

prvalue
Thing(Thing&&)

因此,对于 lvalue 和 xvalue 情况,按值传递比通过重载引用传递要花费额外的移动构造。正如您所指出的,搬家建设并不一定便宜。它可能与复制构造一样昂贵。从好的方面来说,您只需编写 1 个带值传递的重载。通过重载引用传递需要 2^N 重载,其中 N 是参数的数量。在 N==1 时相当可行,在 N==3 时变得笨拙。

此外,正如您所指出的,您的第二个例子只是您的第一个例子加剧了。

当性能是您最关心的问题时,尤其是当您不能指望廉价的移动构造函数时,请通过重载的右值引用传递。当您可以依靠便宜的移动构造,和/或您不想处理不合理的(您可以为自己定义不合理的)数量的重载时,请使用按值传递。没有一个答案在任何情况下都对每个人都是正确的。C++11 程序员仍然需要思考。

于 2013-02-17T17:26:38.700 回答
1

经常被引用的想要速度?按值传递。给这个问题一个详细的处理:

...虽然编译器通常需要在函数参数按值传递时进行复制(因此对函数内部参数的修改不会影响调用者),但允许省略副本,只需使用源对象本身,当源是右值时。

于 2013-02-17T13:29:43.210 回答
1

您没有将其作为选择,但我可以建议完美转发吗?

class Doer
{
public:
    template<typename T>
    Doer(T&& thing) : m_Thing(std::forward<T>(thing)) {}

private:
    Thing m_Thing;
};

这将导致左值/右值的单个复制/移动,这正是我们想要的。

于 2013-02-17T18:00:46.780 回答
0

仅部分回答,但在您给出的第二个示例中,可以通过显式继承 A 的基本 ctor 来避免这些移动:

struct B : A
{
    using A::A;
};

struct C : B
{
    using A::A;
};

struct D : C
{
    using A::A;
};

在这里,您最终会得到 1 个副本对 1 个动作。

于 2013-02-17T13:46:42.383 回答
-6

以下是一些可以解决问题的附加规则:

  1. 永远不要从具体类派生。一个具体的、可实例化的类已经完全实现了。从它派生是对继承的不良使用。=> 你的构造函数参数成本消失了。
  2. 根据数据是在当前对象内部还是外部,决定数据成员中的按值传递和按常量引用传递。
  3. 如果一个类中有构造函数参数,那么同一个类中也应该有一个数据成员。将参数传递给基类不是一个好主意。
  4. 忘记 std::move。复制数据的成本不够大。程序员在任何地方键入 std::move 所花费的时间比实践节省的执行时间要多。
  5. 如果您不断地创建和销毁这些对象,那将比您使用 std::move 保存的任何时间花费更多的时间。
于 2013-02-17T13:16:50.613 回答