C++11 最佳实践:何时接受价值与恒量?

C++11 Best Practice: When to accept by value vs const&?

本文关键字:最佳 何时接 C++11      更新时间:2023-10-16

我对"规则"很满意,当你打算存储对象时,按值接受,当你只需要访问它时,按常量引用接受。这样,你(类的编写者)就不会选择是复制类变量的用户,还是移动它,他们会选择。但在使用过程中,我越来越不确定这个建议的合理性移动在成本操作上不是统一的。。。

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次复制还是5次移动。

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

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

我最好的建议是做你正在做的事:自己想想。不要相信你听到的一切。测量

下面我获取了您的代码,并用print语句对其进行了插入。我还添加了第三种情况:从prvalue初始化:

测试尝试两种方式:

  1. 传递值
  2. 经过const&&&时过载:

代码:

#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 << "lvaluen";
    Doer doer1(thing); // user can decide to copy 'thing'
    std::cout << "nxvaluen";
    Doer doer2(std::move(thing)); // user can decide to move 'thing'
    std::cout << "nprvaluen";
    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的重载引用时,传递值需要额外的move构造。正如你所指出的,移动建筑并不一定便宜。它可能和复制结构一样昂贵。从好的方面来说,您只需要编写1个带有传递值的重载。传递重载引用需要2^N个重载,其中N是参数数。在N==1时非常可行,在N==3时变得笨拙。

正如你所指出的,你的第二个例子只是你的第一个例子。

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

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

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

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

经常被引用的想要速度?传递值。对这个问题进行了详细的处理:

尽管当函数参数通过值传递时,编译器通常需要进行复制(因此对函数内部参数的修改不会影响调用方),但当源是右值时,编译器可以省略复制,只使用源对象本身。

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

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

在这里,你将以1次复制对1次移动结束。

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

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