11

この投稿は少しとりとめのないものなので、本題に入る前に、私が尋ねていることを明確にしたいと思います: コードに move 対応のセッターを追加しましたか? 努力する価値があると思いましたか? そして、私が見つけた動作のどの程度がコンパイラ固有のものであると期待できますか?

ここで注目しているのは、複雑な型のプロパティを設定する場合に、move 対応のセッター関数を追加する価値があるかどうかです。ここでは、移動が有効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;
};

一般的に言えば、過去に非組み込み型のセッター関数に const-ref を使用していました。私が調べたオプションは、値で渡してから move ( SetByMovedCopy)、const-ref で渡してから copy ( SetByConstRef)、最後に r-value-ref で受け入れてから move ( SetByMove) でした。ベースラインとして、値渡しとコピー ( SetByCopy) も含めました。FWIW、値渡しと右辺値参照のオーバーロードの両方を含めると、コンパイラはあいまいさを訴えました。

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にコピーされ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")

valueは からコピー構築されbar_one、次にvalueに移動されます。その後、おそらくより低いコストで、関数の終了後に_barガットが破棄されます。value1 回のコピー操作と 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

tl; dr:PassByValueを使用します。PassByValueで、を介して割り当てますstd::move。セッターも使用していることがわかっている場合はstd::move、セッターを呼び出すときに意味がある場合はいつでも使用してください(つまり) 。foo.PassByValue(std::move(my_local_var))

セッターのその単一バージョンは、オブジェクトを値で取得し、最も一般的な使用法を効率的な方法で処理し、コンパイラーが最適化を実行できるようにし、よりクリーンで読みやすくします。


私は与えられた答えが好きですが、私の質問に対する最良の答えは、私がこれらの方法を別の角度からテストする方法にアプローチするように導いた元の質問のコメントから来たと思います。男は私自身の質問に答えを提供します。

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値参照として渡す。

このテストの目的は、関数呼び出しのオーバーヘッドを測定することではありませんでした。それはマイクロ最適化の領域にあります。私がやろうとしているのは、コンパイラの動作を分析し、セッター関数を実装して使用するためのベストプラクティスを見つけることです。

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値テストで少し優れたパフォーマンスを示しました。vector再割り当てする必要がなく、内容を真っ直ぐにコピーするだけでよいことに気付いたので、l値テストではるかに優れたパフォーマンスを示したと思います。ただし、移動の場合は、割り当てが解除され、そのコストが発生します。しかし、これはvector特定の動作であり、このコンテキストで多くのことを考慮すべきかどうかはわかりません。

それ以外の結果は同様でした。記載されている許容誤差は、結果の標準誤差に基づいており、使用したCPUタイマーの精度は考慮されていません。

私が導き出す結論は、値を渡すだけの方が良いということです。この不自然なシナリオでは、2つの方法はパフォーマンスの点でほぼ同じであり、政府の作業には確かに十分ですが、実装の容易さと値渡しを使用したインターフェイスの明確さにより、私の本では優位に立っています。std::moveセッターを呼び出すときは、それが大きな違いを生む可能性があるので、そうすることが理にかなっているときに使用することを覚えておく必要があります。

私をこの方向に向けてくれた@Luc_Dantonへの帽子のヒント。

于 2012-05-22T17:01:19.563 に答える
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_assignableenable_if(関数が他の特性チェックに誤って表示されないように、を省略しないでください。)

于 2012-05-21T20:54:00.330 に答える
0

私が使用した手法の 1 つは、マクロを使用して、クラス属性のゲッター/セッターを自動的に生成することです。ボイラープレート コードが少なくなるだけでなく、マクロを使用して 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 ゲッターよりも明示的です。)これは、プリプロセッサがコンマで分割されるため、テンプレートをマクロ引数として使用する場合ですが、COMMA マクロを定義することでこれを克服できます。

#define COMMA ,

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