11

这篇文章有点啰嗦,所以在我开始之前,我想弄清楚我在问什么:你是否在你的代码中添加了启用移动的设置器并且你发现它值得付出努力?我发现有多少行为可能是特定于编译器的?

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

一般来说,过去我为非内置类型的 setter 函数使用了 const-ref。我看到的选项是按值传递然后移动(SetByMovedCopy),通过 const-ref 然后复制(SetByConstRef),最后通过 r-value-ref 接受然后移动(SetByMove)。作为基线,我还包括按值传递然后复制(SetByCopy)。FWIW,如果同时包含按值传递和 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")

value从 复制构造bar_one,然后value复制到barvalue被破坏并产生破坏完整对象的任何成本。执行 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")

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

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

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


所以 const-ref 和 move 版本在这种情况下是最有效的。现在,更重要的是,我想要做的是这样的事情:

void SetBar(Bar const & value) { _bar = value; }
void SetBar(Bar && value) { _bar = std::move(value); }

我在这里发现的是,如果您调用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的地方应用来获得更多收益。std::move

4

3 回答 3

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这样你的函数就不会错误地出现在其他特征检查中。)

于 2012-05-21T20:54:00.330 回答
8

tl;博士:使用 PassByValue。在您的 PassByValue 中,通过std::move. 如果您知道 setter 也在使用它,则在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;
};

为了进行测试,我比较了 3 种情况:传递左值、传递右值以及传递显式移动的左值作为右值引用。

此测试的目的不是测量函数调用的开销。那是在微优化领域。我正在尝试做的是剖析编译器行为并制定实现和使用 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-value 测试中的表现要好得多,因为vector它意识到它不需要重新分配并直接复制内容。但是,在移动的情况下,无论如何它都会取消分配,并产生该成本。但这是vector特定的行为,我不确定在这种情况下它是否应该很重要。

否则结果是相似的。列出的误差范围只是基于结果的标准误差,并没有考虑我使用的 CPU 定时器的准确性。

我得出的结论是,最好只传递价值。对于这个人为的场景,这两种方法在性能方面几乎相同,并且对于政府工作来说当然足够好,但是使用传递值的易于实施和界面的清晰性使其在我的书中具有优势。我只需要记住std::move在调用 setter 时使用它是有意义的,因为它可以产生重大影响。

向@Luc_Danton 致敬,因为他为我指明了这个方向。

于 2012-05-22T17:01:19.563 回答
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 );
};

我实际上认为这比一堆 setter 和 getter 更容易阅读。(包含可变访问器有一个很好的理由:这意味着您不必创建完整的副本,而是可以就地修改成员,而且它比非常量 getter 更明确。)这有点麻烦是当您使用模板作为宏参数时,因为预处理器将以逗号分隔,但您可以通过定义 COMMA 宏来克服这个问题:

#define COMMA ,

struct blah {
  ATTRIBUTE( std::map< foo COMMA bar >, baz );
};
于 2012-05-21T21:00:56.460 回答