jbg*_*bgs 63 c++ pass-by-value rvalue-reference move-semantics c++11
由于我们在C++中移动了语义,所以现在通常这样做
void set_a(A a) { _a = std::move(a); }
Run Code Online (Sandbox Code Playgroud)
原因是,如果a是左值,则副本将被删除,并且只有一个移动.
但如果a是左值,会发生什么?似乎将有一个复制结构,然后是一个移动赋值(假设A有一个适当的移动赋值运算符).如果对象具有太多成员变量,则移动分配可能成本很高.
另一方面,如果我们这样做
void set_a(const A& a) { _a = a; }
Run Code Online (Sandbox Code Playgroud)
只有一个副本分配.如果我们传递左值,我们可以说这种方式优于传值的习语吗?
Cas*_*sey 43
在现代C++中,昂贵的移动类型很少见.如果您担心移动的成本,请写两个重载:
void set_a(const A& a) { _a = a; }
void set_a(A&& a) { _a = std::move(a); }
Run Code Online (Sandbox Code Playgroud)
或完美转发的二传手:
template <typename T>
void set_a(T&& a) { _a = std::forward<T>(a); }
Run Code Online (Sandbox Code Playgroud)
这将接受左值,右值和任何其他可隐式转换为decltype(_a)无需额外副本或移动的东西.
尽管从左值设置时需要额外的移动,但成语并不坏,因为(a)绝大多数类型提供恒定时间移动和(b)复制和交换在单个中提供异常安全性和接近最佳性能代码行.
Ali*_*Ali 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)) { }
};
Run Code Online (Sandbox Code Playgroud)
如果您有多个参数,则会出现排列问题.在这个非常简单的例子中,维护这4个构造函数可能仍然不是那么糟糕.但是,在这个简单的情况下,我会认真考虑使用单值函数的pass-by-value方法
C(A a, B b) : a(move(a)), b(move(b)) { }
而不是上面的4个构造函数.
长话短说,这两种方法都没有缺点.根据实际的分析信息做出决策,而不是过早优化.
对于存储价值的一般情况,按值传递只是一个很好的妥协 -
对于你知道只会传递左值(一些紧密耦合的代码)的情况,它是不合理的,不智能的.
对于通过同时提供两者来怀疑速度提升的情况,首先考虑两次,如果这没有帮助,请测量.
在不存储值的情况下,我更喜欢通过引用传递,因为这可以防止不必要的复制操作.
最后,如果编程可以简化为不假思索的规则应用,我们可以将它留给机器人.所以恕我直言,如此关注规则并不是一个好主意.更好地关注不同情况下的优势和成本.成本不仅包括速度,还包括代码大小和清晰度.规则通常不能处理此类利益冲突.
当前的答案还很不完整。相反,我将尝试根据我发现的优缺点列表做出结论。
简而言之,这可能还可以,但有时也很糟糕。
与转发模板或不同的重载相比,这种习惯用法(即统一界面)具有更好的清晰度(在概念设计和实现上)。有时与复制和交换一起使用(实际上,在这种情况下也包括移动和交换)。
优点是:
constconst,而且与兼容volatile,从而减少了更多的常规重载。
const }组合为ñ参数。constconst volatileT,则它可能仍会与参数const T&位于相同位置的重载发生冲突,因为参数可以是type的左值,T而用type实例化的模板T&(而不是const T&)则可能更多。如果没有其他方法可以区分哪个是最佳的重载候选,则按重载规则首选。这种不一致可能非常令人惊讶。P&&,其类中有一个type类型的参数C。您会忘记多少时间P&&从CSFINAE 排除可能具有cv资格的实例中删除实例(例如,通过添加typename = enable_if_t<!is_same<C, decay_t<P>>到template-parameter-list),以确保它不会与copy / move构造函数冲突(即使后者显式)用户提供)?constexpr在某个类中声明的静态数据成员而没有类外定义,当将其用作左值引用类型的参数的参数时,它最终可能无法链接,因为它是odr -使用,没有定义。
constexpr数据成员的规则已更改为隐式引入定义,因此在这种情况下,差异并不明显。缺点是:
noexcept为const&和&&限定类型的参数指定不同的-specifier 。
noexcept(false)copy + noexceptmove noexcept,或者noexcept(false)在您未指定任何内容(或未指定noexcept(false))时始终为您提供copy + move 。(请注意,在前一种情况下,noexcept不能防止在复制过程中抛出异常,因为这只会在函数参数之外的参数求值期间发生。)没有进一步的机会分别调整它们。noexcept自C ++ 17起,的示例可能特别有问题,因为noexcept-规范现在会影响函数类型。(某些意外的兼容性问题可以通过Clang ++警告来诊断。)有时无条件复制实际上是有用的。由于具有强例外保证的业务构成本质上不具有担保,因此,在需要强例外保证且操作不能按严格的操作顺序分解的情况下,可以将副本用作跨国国家持有人(无例外或强烈)例外保证。(这包括复制和交换习惯用法,尽管通常不建议出于其他原因将分配统一,请参见下文。)但是,这并不意味着复制是不可接受的。如果接口的意图始终是创建某种类型的对象T,并且移动的成本T是可忽略的,则可以将副本移动到目标而不会产生不必要的开销。
因此,对于某些给定的操作,以下是有关是否使用统一接口来替换它们的建议:
T,如果所有操作都需要每个参数的副本,请使用unification。T具有可忽略的成本,则使用统一。T,而移动构造的成本T是可忽略的,则使用统一。以下是一些需要避免统一的示例:
T在复制和移动构造中没有可忽略的成本的分配操作(包括分配给其子对象,通常具有复制和交换习惯)不符合统一的标准,因为分配的目的不是创建(而是替换)。对象的内容。复制的对象最终将被破坏,这将导致不必要的开销。对于自我分配的情况,这一点更加明显。std::map::insert_or_assign即使发生上述故障,仍会插入类似的容器),这会产生开销。请注意,“可忽略”成本的准确限制在一定程度上是主观的,因为它最终取决于开发人员和/或用户可以承受的成本,并且可能会因情况而异。
实际上,我(保守地)假设其大小不超过一个机器字(如指针)的琐碎可复制且琐碎的可破坏类型通常符合可忽略成本的标准-如果在这种情况下所产生的代码实际成本过高,建议使用错误的构建工具配置,或者工具链尚未准备好投入生产。
如果对性能还有任何疑问,请进行分析。
根据约定,还有一些其他众所周知的类型优选按值传递或不按值传递:
std::bind。另请参阅LWG 817的分辨率。std::move,因为假定副本的成本是可忽略的,并且此举不一定会使它更好。
std::function(但不是赋值运算符模板)也使用按值传递函子参数。std::initializer_list和的实例std::basic_string_view或多或少是两个指针或一个指针加一个大小。这个事实使它们便宜到足以直接使用而不使用引用。shared_ptr实例。(但是,并非所有智能指针都像那样;observer_ptr更像是原始指针。)| 归档时间: |
|
| 查看次数: |
13937 次 |
| 最近记录: |