Herb SutterのCppCon 2014の話で、基本に戻る:彼はスライド28( スライドのWebコピーはこちら )でこのパターンについて言及しています:
class employee {
std::string name_;
public:
void set_name(std::string name) noexcept { name_ = std::move(name); }
};
一時的にset_name()を呼び出すとnoexcept-nessが強くないため、これは問題があると彼は言います(彼は「noexcept-ish」というフレーズを使用しています)。
今、私は自分の最近のC++コードで上記のパターンをかなり頻繁に使用しています。これは、主に毎回set_name()の2つのコピーを入力する手間を省くためです。でもねえ、私は怠惰なタイパーです。ただし、ハーブのフレーズ「This noexcept isproblematic」は、ここでは問題が発生しないので心配です:std :: stringの移動代入演算子はnoexceptであるため、はそのデストラクタであるため、上のset_name()はnoexceptが保証されているように思えます。コンパイラbeforeset_name()がパラメータを準備するときに例外がスローされる可能性があることはわかりますが、問題があると考えて苦労しています。
後のスライド32で、ハーブは上記がアンチパターンであることを明確に述べています。誰かが怠惰で悪いコードを書いていないのはなぜか私に説明できますか?
他の人は上記のnoexcept
の推論をカバーしています。
ハーブは効率性の面での話に多くの時間を費やしました。問題は割り当てではなく、不必要な割り当て解除です。コピーするときstd::string
別のコピールーチンは、コピーされるデータを保持するのに十分なスペースがある場合、コピー先の文字列に割り当てられたストレージを再利用します。移動割り当てを行う場合、宛先文字列の既存のストレージは、ソース文字列からストレージを引き継ぐため、割り当てを解除する必要があります。 「コピーと移動」のイディオムは、一時変数を渡さない場合でも、常に割り当て解除を強制します。これは、講演の後半で示される恐ろしいパフォーマンスの原因です。彼のアドバイスは、代わりにconst refを取得することであり、r値参照のオーバーロードが必要だと判断した場合です。これにより、両方のメリットが得られます:非一時的な割り当てを解除するために既存のストレージにコピーし、一時的に割り当てを解除するために一時的に移動します(移動先に移動先の割り当てを解除するか、ソースはコピー後に割り当て解除されます)。
メンバー変数に割り当てを解除するためのストレージがないため、上記はコンストラクターには適用されません。コンストラクターは複数の引数をとる場合が多く、各引数に対してconst ref/r-value refオーバーロードを実行する必要がある場合、コンストラクターオーバーロードの組み合わせが急増することになります。
質問は次のようになります:コピー時にstd :: stringのようなストレージを再利用するクラスはいくつありますか? std :: vectorはそうだと思いますが、それ以外はわかりません。このようなストレージを再利用するクラスを書いたことがないのは確かですが、文字列とベクトルを含むクラスをたくさん書いています。 Herbのアドバイスに従っても、ストレージを再利用しないクラスに害を及ぼすことはありません。最初は、シンク関数のコピーバージョンを使用してコピーすることになります。コピーがパフォーマンスヒットの大きすぎると判断した場合は、コピーを回避するために、r値参照オーバーロードを作成します(std :: stringの場合と同じように)。一方、「コピーアンドムーブ」を使用すると、std :: stringやストレージを再利用するその他のタイプのパフォーマンスに影響が出ます。これらのタイプは、ほとんどの人のコードで多くの使用が見られます。私は今のところハーブのアドバイスに従っていますが、問題が完全に解決されたと考える前に、これについてもう少し検討する必要があります(おそらく、このすべてに潜む時間がないブログ投稿があります)。
値による受け渡しがconst参照による受け渡しよりも優れている理由は2つ考えられます。
タイプstd::string
のメンバーのセッターの場合、彼は、値による受け渡しの方が、const参照による受け渡しが通常は少ない割り当て(少なくともstd::string
の場合)をもたらすことを示すことで、より効率的であるという主張を覆しました。
また、パラメータをコピーするプロセスで例外が発生する可能性があるため、noexcept宣言が誤解を招く可能性があることを示すことにより、セッターがnoexcept
を許可するという主張を明らかにしました。
したがって、少なくともこの場合、const参照による受け渡しは、値による受け渡しよりも優先されると結論付けました。ただし、値による受け渡しはコンストラクターにとって潜在的に優れたアプローチであると述べました。
std::string
の例だけでは、すべてのタイプに一般化するには不十分だと思いますが、少なくとも、コピーするのにコストがかかりますが移動するのが安いパラメーターを値で渡すという慣習に疑問を投げかけます。効率と例外の理由。
Herbには、すでにストレージが割り当てられているときに値渡しを行うと効率が悪くなり、不要な割り当てが発生する可能性があるという点があります。ただし、const&
による取得は、生のC文字列を取得して関数に渡す場合と同じように、ほとんど同じように悪いものです。
必要なのは、文字列そのものではなく、文字列から読み取るという抽象化です。
これでtemplate
としてこれを行うことができます:
class employee {
std::string name_;
public:
template<class T>
void set_name(T&& name) noexcept { name_ = std::forward<T>(name); }
};
これはかなり効率的です。次に、SFINAEを追加します。
class employee {
std::string name_;
public:
template<class T>
std::enable_if_t<std::is_convertible<T,std::string>::value>
set_name(T&& name) noexcept { name_ = std::forward<T>(name); }
};
そのため、実装ではなくインターフェイスでエラーが発生します。
実装を公開する必要があるため、これは常に実用的であるとは限りません。
これがstring_view
タイプのクラスの出番です。
template<class C>
struct string_view {
// could be private:
C const* b=nullptr;
C const* e=nullptr;
// key component:
C const* begin() const { return b; }
C const* end() const { return e; }
// extra bonus utility:
C const& front() const { return *b; }
C const& back() const { return *std::prev(e); }
std::size_t size() const { return e-b; }
bool empty() const { return b==e; }
C const& operator[](std::size_t i){return b[i];}
// these just work:
string_view() = default;
string_view(string_view const&)=default;
string_view&operator=(string_view const&)=default;
// myriad of constructors:
string_view(C const* s, C const* f):b(s),e(f) {}
// known continuous memory containers:
template<std::size_t N>
string_view(const C(&arr)[N]):string_view(arr, arr+N){}
template<std::size_t N>
string_view(std::array<C, N> const& arr):string_view(arr.data(), arr.data()+N){}
template<std::size_t N>
string_view(std::array<C const, N> const& arr):string_view(arr.data(), arr.data()+N){}
template<class... Ts>
string_view(std::basic_string<C, Ts...> const& str):string_view(str.data(), str.data()+str.size()){}
template<class... Ts>
string_view(std::vector<C, Ts...> const& vec):string_view(vec.data(), vec.data()+vec.size()){}
string_view(C const* str):string_view(str, str+len(str)) {}
private:
// helper method:
static std::size_t len(C const* str) {
std::size_t r = 0;
if (!str) return r;
while (*str++) {
++r;
}
return r;
}
};
このようなオブジェクトはstd::string
または"raw C string"
から直接構築でき、そこから新しいstd::string
を生成するために必要な情報をほぼコストをかけずに格納できます。
class employee {
std::string name_;
public:
void set_name(string_view<char> name) noexcept { name_.assign(name.begin(),name.end()); }
};
そして、現在のset_name
には固定インターフェース(完全なフォワードインターフェースではありません)があるため、実装が表示されないことがあります。
唯一の非効率性は、Cスタイルの文字列ポインターを渡すと、サイズが2倍になることです(最初は'\0'
を探し、2回目はそれらをコピーします)。一方、これはターゲットの情報に必要な大きさを提供するため、再割り当てではなく事前割り当てを行うことができます。
そのメソッドを呼び出す方法は2つあります。
rvalue
パラメータを使用すると、パラメータタイプのmove constructor
がnoexceptがない限り(std::string
の場合はおそらくnoexceptはありません)、どの場合でもより適切に使用されます条件付きnoexcept(パラメーターがnoexceptであることを確認するため)lvalue
パラメータを使用すると、この場合、パラメータタイプのcopy constructor
が呼び出され、ほぼ確実に割り当てが必要になります(スローする可能性があります)。このような使用が誤って使用される可能性がある場合は、回避することをお勧めします。 class
のクライアントは、指定どおりに例外がスローされないことを前提としていますが、有効でコンパイル可能であり、不審ではないC++11
がスローする可能性があります。