是否值得添加一个支持移动的setter?

Mik*_*e E 7 c++ visual-studio-2010 move-semantics c++11

这个帖子在我进入它之前有点摇摆我想清楚我在问什么:你是否已经为你的代码添加了启用移动的setter并且你发现它值得付出努力吗?我发现我可以发现的行为有多少可能是特定于编译器的?

我在这里看到的是,在我设置复杂类型的属性的情况下,添加启用移动的setter函数是否值得.这里我启用了移动功能Bar,Foo并且具有Bar可以设置的属性.

class Bar {
public:
    Bar() : _array(1000) {}
    Bar(Bar const & other) : _array(other._array) {}
    Bar(Bar && other) : _array(std::move(other._array)) {}
    Bar & operator=(Bar const & other) {
        _array = other._array;
        return *this;
    }
    Bar & operator=(Bar && other) {
        _array = std::move(other._array);
        return *this;
    }
private:
    vector<string> _array;
};

class Foo {
public:
    void SetBarByCopy(Bar value) {
        _bar = value;
    }
    void SetBarByMovedCopy(Bar value) {
        _bar = std::move(value);
    }
    void SetBarByConstRef(Bar const & value) {
        _bar = value;
    }
    void SetBarByMove(Bar && value) {
        _bar = std::move(value);
    }
private:
    Bar _bar;
};
Run Code Online (Sandbox Code Playgroud)

一般来说,在过去,我已经使用了const-ref来获取非内置类型的setter函数.我看到的选项是传递by-value然后move(SetByMovedCopy),传递const-ref然后复制(SetByConstRef),最后通过r-value-ref接受move(SetByMove).作为基线,我还包括pass-by-value然后copy(SetByCopy).FWIW,如果包含pass-by-value和r-value-ref重载,编译器会抱怨含糊不清.

在使用VS2010编译器的实验中,这是我发现的:

Foo foo;
Bar bar_one;

foo.SetByCopy(bar_one);
// Bar::copy ctor called (to construct "value" from bar_one)
// Foo::SetByCopy entered
// Bar::copy operator= called (to copy "value" to _bar)
// Foo::SetByCopy exiting
// Bar::dtor called (on "value")
Run Code Online (Sandbox Code Playgroud)

value是复制构造bar_one,然后value复制到bar.value被破坏并产生破坏完整物体的任何费用.执行2次复制操作.

foo.SetByMovedCopy(bar_one);
// Bar::copy ctor called (to construct "value" from bar_one)
// Foo::SetByCopy entered
// Bar::move operator= called (to move "value" into _bar)
// Foo::SetByCopy exiting
// Bar::dtor called (to destruct the moved "value")
Run Code Online (Sandbox Code Playgroud)

value是复制构造bar_one,然后value被移入_bar,然后value在功能退出后去内脏被破坏,可能是以较低的成本.1份复印和1次移动操作.

foo.SetByConstRef(bar_one);
// Foo::SetByConstRef entered
// Bar::copy operator= called (to copy bar_one into _bar)
// Foo::SetByConstRef exiting
Run Code Online (Sandbox Code Playgroud)

bar_one被直接复制到_bar.1份复制操作.

foo.SetByMove(std::move(bar_one))
// Foo::SetByMove entered
// Bar::move operator= called (to move "value" into _bar)
// Foo::SetByMove exited
Run Code Online (Sandbox Code Playgroud)

bar_one直接进入_bar.1移动操作.


因此const-ref和move版本在这种情况下效率最高.现在,更重要的是,我要做的是这样的事情:

void SetBar(Bar const & value) { _bar = value; }
void SetBar(Bar && value) { _bar = std::move(value); }
Run Code Online (Sandbox Code Playgroud)

我在这里发现的是,如果你调用Foo::SetBar,编译器会根据你传递的是l值还是r值来选择函数.您可以通过这样调用来强制解决问题std::move:

foo.SetBar(bar_one); // Const-ref version called
foo.SetBar(Bar()); // Move version called
foo.SetBar(std::move(bar_one)); // Move version called
Run Code Online (Sandbox Code Playgroud)

我不禁想到添加所有这些移动设置器,但我认为它可能会导致一个非常显着的性能增益,在临时传递给SetBar函数并继续前进的情况下,我可以通过std::move适当的应用获得更多.

Ker*_* SB 8

另一种选择是模板:

template <typename T>
typename std::enable_if<std::is_assignable<Foo, T>::value>::type set(T && t)
{
    foo_ = std::forward<T>(t);
}
Run Code Online (Sandbox Code Playgroud)

这样你就可以匹配任何可转换的东西和任何价值类别.别忘#include <type_traits>了得到is_assignable.(你不应该省略enable_if你的功能,以免你的功能错误地显示在其他特征检查中.)

  • @Bloodtoes:IIRC,VC10*确实*包含完整的`type_traits`标题,并且还具有完美的转发功能,因此这应该可行.它甚至有`decltype`,这意味着你可以自己轻松实现`is_assignable`(或者甚至从Boost中获取它). (2认同)

Mik*_*e E 6

tl; dr:使用PassByValue.在PassByValue中,分配通过std::move.使用std::move时,它是有道理这样做调用setter方法(即当foo.PassByValue(std::move(my_local_var)))如果你知道,二传手也用它.

单个版本的setter,以对象为按值,以有效的方式处理最常见的用法,允许编译器进行优化,更清晰,更易读.


我喜欢给出的答案,但我认为我的问题的最佳答案来自原始问题中的评论,这些评论引导我探讨如何从不同的角度测试这些方法,所以我将会是那样的家伙提供了我自己的问题的答案.

class Foo {
public:
    void PassByValue(vector<string> value) {
        _bar = std::move(value);
    }
    void PassByConstRefOrMove(vector<string> const & value) {
        _bar = value;
    }
    void PassByConstRefOrMove(vector<string> && value) {
        _bar = std::move(value);
    }
    void Reset() {
        std::swap(_bar, vector<string>());
    }
private:
    vector<string> _bar;
};
Run Code Online (Sandbox Code Playgroud)

为了测试,我比较了3种情况:传递l值,传递r值,并将显式移动的l值作为r值引用传递.

此测试的目的不是测量函数调用的开销.这就是微优化领域.我要做的是剖析编译器行为并制定实现和使用setter函数的最佳实践.

vector<string> lots_of_strings(1000000, "test string");
Foo foo;
// Passing an l-value
foo.PassByValue(lots_of_strings);
// Passing an r-value
foo.PassByValue(vector<string>(1000000, "test string"));
// Passing an r-value reference
foo.PassByValue(std::move(lots_of_strings));

// Reset vector because of move
lots_of_strings = vector<string>(1000000, "test string");
// l-value, calls const-ref overload
foo.PassByConstRefOrMove(lots_of_strings);
// r-value, calls r-value-ref overload
foo.PassByConstRefOrMove(vector<string>(1000000, "test string"));
// explicit move on l-value, calls r-value-ref overload
foo.PassByConstRefOrMove(std::move(lots_of_strings));
Run Code Online (Sandbox Code Playgroud)

为简洁而排除的是我Foo::Reset()在每次打电话后都打电话给我_bar.结果(1000次通过后):

PassByValue:
  On l-value    : 34.0975±0.0371 ms
  On r-value    : 30.8749±0.0298 ms
  On r-value ref:  4.2927e-3±4.2796e-5 ms

PassByConstRefOrMove:
  On l-value    : 33.864±0.0289 ms
  On r-value    : 30.8741±0.0298 ms
  On r-value ref:  4.1233e-3±4.5498e-5 ms
Run Code Online (Sandbox Code Playgroud)

foo每次通话后重置可能不是对现实生活的完美模拟.当我没有这样做而是设置_bar已经有一些数据时,PassByConstRef在l值测试上表现更好,在r值测试上表现更好.我相信它在l值测试中表现得更好,因为vector意识到它不需要重新分配并直接复制内容.在移动的情况下,无论如何都会取消分配,并产生成本.但这是一种vector特定的行为,我不确定它在这种情况下是否应该算得多.

否则结果是相似的.列出的误差范围仅基于结果的标准误差,并未考虑我使用的CPU计时器的准确性.

我得出的结论是,通过价值传递会更好.对于这种人为设计的场景,这两种方法在性能方面几乎完全相同,对于政府工作来说肯定足够好,但是实现的简易性和使用按值传递的界面清晰度使其成为我书中的优势.我只需要记住std::move在调用setter时使用它是有意义的,因为它可以产生显着的差异.

给@Luc_Danton的帽子提示我指向这个方向.

  • 我希望有人仍然阅读这个线程。我不清楚为什么这个函数调用:`foo.PassByConstRefOrMove(vector&lt;string&gt;(1000000, "test string"));` 没有调用移动设置器。在这种情况下,这样的临时对象不是被解释为右值引用吗?我认为这是唯一有意义的解释,并且通过比较执行时间,似乎没有调用移动设置器。 (2认同)