由于我们已经在C ++中移动了语义,现在通常会这样做
void set_a(A a) { _a = std::move(a); }
原因是,如果a
是左值,则副本将被删除,并且只有一次移动。
但如果a
是左值,会发生什么?似乎将有一个复制结构,然后是一个移动赋值(假设A有一个适当的移动赋值运算符)。如果对象具有太多成员变量,则移动分配可能成本很高。
另一方面,如果我们这样做
void set_a(const A& a) { _a = a; }
只有一个副本分配。如果我们传递左值,我们可以说这种方式优于传值的习语吗?
答案 0 :(得分:40)
在现代C ++中,昂贵的移动类型很少见。如果您担心移动的成本,请写两个重载:
void set_a(const A& a) { _a = a; }
void set_a(A&& a) { _a = std::move(a); }
或完美转发的二传手:
template <typename T>
void set_a(T&& a) { _a = std::forward<T>(a); }
将接受左值,右值和其他任何可隐式转换为decltype(_a)
而无需额外副本或移动的内容。
尽管从左值设置时需要额外的移动,但成语不是坏,因为(a)绝大多数类型提供恒定时间移动和(b)复制和交换提供单行代码中的异常安全性和接近最佳性能。
答案 1 :(得分:23)
但如果
a
是左值,会发生什么?似乎会有副本 施工,然后移动任务(假设A有适当的移动 赋值运算符)。如果对象具有移动分配可能是昂贵的 成员变量太多了。
问题很明显。我不会说,传递价值然后移动的结构是一个不好的习惯,但它肯定有其潜在的陷阱。
如果您的类型移动和/或移动的代价很高,它基本上只是一个副本,那么按值传递方法是次优的。此类类型的示例将包括具有固定大小数组作为成员的类型:移动可能相对昂贵并且移动仅仅是副本。另见
在这种情况下。
按值传递方法的优点是您只需要维护一个功能,但是您需要为此付出代价。这取决于您的应用程序,这种维护优势是否超过性能损失。
如果您有多个参数,通过左值和左值参考方法传递可能会导致维护问题。请考虑以下事项:
#include <vector>
using namespace std;
struct A { vector<int> v; };
struct B { vector<int> v; };
struct C {
A a;
B b;
C(const A& a, const B& b) : a(a), b(b) { }
C(const A& a, B&& b) : a(a), b(move(b)) { }
C( A&& a, const B& b) : a(move(a)), b(b) { }
C( A&& a, B&& b) : a(move(a)), b(move(b)) { }
};
如果您有多个参数,则会出现排列问题。在这个非常简单的例子中,维护这4个构造函数可能仍然不是那么糟糕。但是,在这个简单的情况下,我会认真考虑使用单值函数的传值方法
C(A a, B b) : a(move(a)), b(move(b)) { }
而不是上面的4个构造函数。
故事很长,这两种方法都没有缺点。根据实际的分析信息做出决定,而不是过早优化。
答案 2 :(得分:8)
对于存储值的一般情况 ,传递值只是一个很好的折衷方案 -
对于你知道只会传递左值(一些紧密耦合的代码)的情况,这是不合理的,不智能的。
对于通过同时提供两者来怀疑速度提升的情况,首先考虑两次,如果这没有帮助,请测量。
如果不存储该值,我更喜欢通过引用传递,因为这可以防止不必要的复制操作。
最后,如果编程可以简化为不假思索的规则应用,我们可以将其留给机器人。所以恕我直言,如此关注规则并不是一个好主意。更好地关注不同情况下的优势和成本。成本不仅包括速度,还包括例如速度。代码大小和清晰度。规则通常不能处理此类利益冲突。
答案 3 :(得分:3)
当前答案还很不完整。取而代之的是,我将根据我发现的优缺点列表做出结论。
简而言之,这可能还可以,但有时很糟糕。
与转发模板或不同的重载相比,该习惯用法即 unifying 界面(在概念设计和实现上)具有更好的清晰度。有时与 copy-and-swap 一起使用(实际上,在这种情况下也与 move-and-swap 一起使用)。
优点是:
const
合格)。const
兼容,而且与volatile
兼容,从而减少了更多的普通重载。
const
,const
,{ n 个参数的{1}}}组合。const volatile
的转发模板,它可能仍与参数T
位于同一位置的重载冲突,因为该参数可以是类型{{ 1}}和用类型const T&
(而不是T
实例化的模板),因为在没有其他方法可以区分哪个是最佳重载候选者的情况下,重载规则可能会更喜欢此模板。这种不一致可能非常令人惊讶。T&
中具有一个类型为const T&
的参数的转发模板构造函数。您会忘记多少时间SFINAE将P&&
的实例排除在可能具有简历资格的C
之外(例如,将P&&
添加到 template-parameter-list em>),以确保它不会与copy / move构造函数冲突(即使后者是由用户明确提供的)?C
数据成员,但没有类外定义,当它用作左值引用类型的参数的参数时,最终可能无法链接,因为it is odr-used并没有定义。
typename = enable_if_t<!is_same<C, decay_t<P>>
数据成员的规则已更改to introduce a definition implicitly,因此在这种情况下,差异并不明显。缺点是:
constexpr
和constexpr
限定的参数指定不同的noexcept
指定符类型。
const&
副本+ &&
移动(如果您指定noexcept(false)
,或者始终为noexcept
,但您未指定(或显式noexcept
)。 (请注意,在前一种情况下,noexcept(false)
不会阻止在复制过程中抛出异常,因为这只会在函数参数之外的参数求值期间发生。)没有进一步的机会来单独调整它们。noexcept(false)
的示例自C ++ 17起可能尤其成问题,因为noexcept
规范dedicated copy elision rules。 (now affect the function type可以诊断出一些意外的兼容性问题。)有时候无条件复制实际上是有用的。由于具有强例外保证的业务构成本质上不具有担保,因此,在需要强例外保证且操作不能按严格的操作顺序分解的情况下,可以将副本用作跨国国家持有人(无例外或强烈)例外保证。 (这包括复制和交换的习惯用法,尽管一般出于其他原因,建议不要 将分配统一,请参见下文。)但是,这并不意味着复制是不可接受的。如果界面的意图是总是 创建noexcept
类型的某个对象,并且移动noexcept
的成本是可忽略的,则可以将副本无目的地移动到目标开销。
因此,对于某些给定的操作,以下是有关是否使用统一界面来替换它们的建议:
T
类型的任何参数,如果所有操作都需要每个参数的副本,请使用统一。T
的复制结构和移动结构的成本都可忽略,则使用统一。T
的对象,并且T
的移动构造的成本是可忽略的,请使用统一。 / li>
这里有一些例子需要避免统一:
T
的分配操作(包括分配给其子对象,通常具有复制和交换习惯)在复制和移动构造中没有可忽略的成本,因此不符合统一标准,因为分配的意图是不是创建(而是替换内容)对象。复制的对象最终将被破坏,这将导致不必要的开销。对于自我分配的情况,这一点更加明显。T
类的容器插入操作)也会产生开销。请注意,“可忽略”成本的准确限制在一定程度上是主观的,因为它最终取决于开发人员和/或用户可以承受的成本,并且可能因情况而异。
实际上,我(保守地)假设任何琐碎可复制且琐碎的可破坏类型,其大小不超过一个机器字(如指针),通常符合可忽略成本的标准-如果结果代码实际上在这种情况下花费太多在这种情况下,则表明要么使用了错误的构建工具配置,要么工具链尚未准备好投入生产。
如果对性能还有任何疑问,请进行个人资料设置。
还有其他一些众所周知的类型,通常可以通过值来传递它们:
T
,因为副本的成本被认为是可忽略的,而且移动并不一定会使它更好。这样的参数包括迭代器和函数对象(Clang++ warning除外)。std::map::insert_or_assign
和std::move
的实例或多或少是两个指针或一个指针加上一个大小。这个事实使它们便宜到足以直接传递而无需引用。
同样,除非需要复制,否则某些类型应避免通过值传递。容器就是这种典型的例子。 *特别是,(argument) forwarding caller wrappers和其他一些类似于分配器的类型(allocator-awared containers中的“容器语义”),即使性能完全不感兴趣,也不应按值传递-分配器传播为只是另一个大的语义蠕虫可以。
其他一些常规类型取决于。例如,请参见David Krauss' word中的std::initializer_list
实例。 (但是,并非所有智能指针都像这样; std::basic_string_view
更像是原始指针。)
答案 4 :(得分:1)
按值传递,然后移动实际上是您知道可以移动的对象的好习惯。
正如你所提到的,如果rvalue被传递,它将要么删除副本,要么被移动,然后在构造函数中移动它。
您可以重载复制构造函数并显式移动构造函数,但是如果您有多个参数,它会变得更复杂。
考虑一下这个例子,
class Obj {
public:
Obj(std::vector<int> x, std::vector<int> y)
: X(std::move(x)), Y(std::move(y)) {}
private:
/* Our internal data. */
std::vector<int> X, Y;
}; // Obj
假设您想提供显式版本,最终会得到4个构造函数,如:
class Obj {
public:
Obj(std::vector<int> &&x, std::vector<int> &&y)
: X(std::move(x)), Y(std::move(y)) {}
Obj(std::vector<int> &&x, const std::vector<int> &y)
: X(std::move(x)), Y(y) {}
Obj(const std::vector<int> &x, std::vector<int> &&y)
: X(x), Y(std::move(y)) {}
Obj(const std::vector<int> &x, const std::vector<int> &y)
: X(x), Y(y) {}
private:
/* Our internal data. */
std::vector<int> X, Y;
}; // Obj
正如您所看到的,当您增加参数数量时,必要构造函数的数量会以排列方式增长。
如果你没有具体的类型,但有一个模板化的构造函数,你可以像这样使用完美转发:
class Obj {
public:
template <typename T, typename U>
Obj(T &&x, U &&y)
: X(std::forward<T>(x)), Y(std::forward<U>(y)) {}
private:
std::vector<int> X, Y;
}; // Obj
参考文献:
答案 5 :(得分:1)
我正在回答自己,因为我会尝试总结一些答案。我们在每种情况下都有多少动作/副本?
(A)按值传递并移动赋值构造,传递X参数。如果X是......
临时:1次移动(副本被删除)
左值:1副本1移动
std :: move(左值):2次移动
(B)通过引用和复制分配通常(前C ++ 11)构造。如果X是......
临时:1份
左值:1份
std :: move(左值):1份
我们可以假设这三种参数是同等可能的。所以每3个电话我们有(A)4个动作和1个副本,或(B)3个副本。即,平均而言,(A)每次通话1.33次移动,每次通话0.33份,或(B)每次通话1次。
如果我们的课程主要由POD组成,那么移动与副本一样昂贵。因此,对于案例(A),每次调用时,我们将有1.66个副本(或移动),对于案例(B),我将有1个副本。
我们可以说在某些情况下(基于POD的类型),传值和后移构造是一个非常糟糕的主意。它慢了66%,它取决于C ++ 11的功能。
另一方面,如果我们的类包含容器(使用动态内存),(A)应该快得多(除非我们主要传递左值)。
如果我错了,请纠正我。
答案 6 :(得分:0)
声明中的可读性:
kubectl apply -f Deployment.yaml -o yaml
性能:
void foo1( A a ); // easy to read, but unless you see the implementation
// you don't know for sure if a std::move() is used.
void foo2( const A & a ); // longer declaration, but the interface shows
// that no copy is required on calling foo().
职责:
A a;
foo1( a ); // copy + move
foo2( a ); // pass by reference + copy
对于典型的内联代码,优化后通常没有区别。 但是foo2()可能仅在某些条件下(例如,如果键不存在,则插入map中)进行复制,而对于foo1(),复制将始终完成。