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

时间:2012-05-21 20:43:55

标签: c++ visual-studio-2010 c++11 move-semantics

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

我在这里看到的是,在我设置复杂类型的属性的情况下,添加启用移动的setter函数是否值得。在这里,我启用了移动BarFoo,其中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;
};

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

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

SetByCopy

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") value复制构建,然后bar_one被复制到valuebar被破坏,并且会产生破坏完整物体的任何费用。执行2次复制操作。

value

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") 是从value复制构建的,然后bar_one被移入value,然后在函数退出后被毁坏的_bar被破坏,大概是以较低的成本。 1次复印和1次移动操作。

value

foo.SetByConstRef(bar_one); // Foo::SetByConstRef entered // Bar::copy operator= called (to copy bar_one into _bar) // Foo::SetByConstRef exiting 会直接复制到bar_one。 1份复制操作。

_bar

foo.SetByMove(std::move(bar_one)) // Foo::SetByMove entered // Bar::move operator= called (to move "value" into _bar) // Foo::SetByMove exited 直接移至bar_one。 1移动操作。


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

_bar

我在这里发现的是,如果你调用void SetBar(Bar const & value) { _bar = value; } void SetBar(Bar && value) { _bar = std::move(value); } ,编译器会根据你传递的是l值还是r值来选择函数。您可以通过调用Foo::SetBar来强制解决问题:

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 函数并且继续前进的情况下,它可能会带来非常显着的性能提升。适当时应用SetBar

3 个答案:

答案 0 :(得分:8)

另一种选择是模板:

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

通过这种方式,您可以匹配任何可转换的和任何值类别。不要忘记#include <type_traits>获取is_assignable。 (你不应该省略enable_if,这样你的功能就不会错误地显示在其他特征检查中。)

答案 1 :(得分:5)

tl; dr:使用PassByValue。在PassByValue中,通过std::move分配。如果您知道setter也在使用它,那么在调用setter(即std::move)时,只要有意义就使用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;
};

为了测试,我比较了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));

为简洁而排除的是,在每次清除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

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

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

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

向@Luc_Danton发送帽子提示我指向这个方向。

答案 2 :(得分:0)

我使用的一种技术是使用宏自动生成类属性的getter / setter。除了较少的样板代码之外,这还有其他优点,例如使用宏自动提供typedef并强制实现一致的接口语义。我发现它比其他代码更难阅读。

例如,

#define ATTRIBUTE(type, name) \
public: \
typedef type name##_type; \
\
void set_##name( type const &p_##name ) { \
  m_##name = p_#name; \
} \
\
void set_##name( type &&p_##name ) { \
  m_##name = std::move( p_##name ); \
\
type const &get_##name( ) const { \
  return m_##name; \
} \
\
type *mutable_##name( ) { \
  return &m_##name; \
} \
\
private: \
\
type m_##name;

现在你的代码看起来像这样:

struct blah {
  ATTRIBUTE( std::string, foo );
};

我实际上认为这比一堆制定者和吸气者更容易阅读。 (包含可变访问器的理由很充分:它意味着您不必创建完整的副本,而是可以在适当的位置修改成员,并且它比非const的getter更明确。)这有点毛茸茸是因为预处理器将以逗号分割,所以当你使用模板作为宏参数时,你可以通过定义COMMA宏来克服这个问题:

#define COMMA ,

struct blah {
  ATTRIBUTE( std::map< foo COMMA bar >, baz );
};