web-dev-qa-db-ja.com

Const std :: string&をパラメータとして渡した日数は過ぎていますか?

std::vectorによってstd::stringconst &を渡す理由はほとんどなくなったと示唆する最近のHerb Sutterによる話を聞いた。彼は、次のような関数を書くことが現在は望ましいと提案しました。

std::string do_something ( std::string inval )
{
   std::string return_val;
   // ... do stuff ...
   return return_val;
}

return_valは、関数が戻る時点で右辺値になるので、Move Semanticsを使用して返すことができ、非常に安価です。ただし、invalは、参照のサイズ(通常はポインタとして実装される)よりもはるかに大きいです。これは、std::stringがヒープへのポインタや短い文字列の最適化のためのメンバchar[]を含むさまざまなコンポーネントを持っているためです。それで、参照渡しはまだ良い考えのようです。

ハーブがなぜこれを言ったのか説明できる人はいますか?

554
Benj

ハーブが彼が言ったことを言った理由はこのようなケースのためです。

関数Aを呼び出す関数Bがあり、関数Cを呼び出すとします。そしてAは文字列をBを通してCに渡します。 ACを知りませんし、気にしません。 Aが知っているのはBだけです。つまり、CBの実装の詳細です。

Aが次のように定義されているとしましょう。

void A()
{
  B("value");
}

BとCがconst&で文字列を取ると、それは次のようになります。

void B(const std::string &str)
{
  C(str);
}

void C(const std::string &str)
{
  //Do something with `str`. Does not store it.
}

すべてが順調です。あなたはただポインタを渡しているだけで、コピーも移動もしなくて、みんな幸せです。 Cは文字列を格納しないのでconst&を取ります。それを使うだけです。

今、私は1つの簡単な変更を加えたいと思います:Cはどこかに文字列を格納する必要があります。

void C(const std::string &str)
{
  //Do something with `str`.
  m_str = str;
}

こんにちは、コンストラクタと潜在的なメモリ割り当てをコピーしてください( Short String Optimization(SSO) を無視してください)。 C++ 11のムーブ・セマンティクスは、不要なコピー作成を排除することを可能にすると思われますね。そしてAは一時的なものを渡します。 C copy にする必要がある理由はありません。それは与えられたことに横たわっているべきです。

それ以外はできません。それはconst&を取るからです。

Cを変更してそのパラメータを値で受け取るようにした場合、それはBにそのパラメータへのコピーを実行させるだけです。私は何も得ません。

そのため、データをシャッフルするためにstd::moveを使用して、すべての関数にstrを値で渡しただけでは、この問題は起こりません。誰かがそれに固執したい場合は、そうすることができます。そうでなければ、まあまあ。

もっと高価ですか?はい;値に移動すると、参照を使用するよりも費用がかかります。コピーよりも安いですか? SSO付きの小さい文字列には使用できません。やる価値がありますか?

それはあなたのユースケースによります。あなたはどのくらいメモリ割り当てを嫌いですか?

371
Nicol Bolas

Const std :: string&をパラメーターとして渡す日は終わりましたか?

いいえ。多くの人は、このアドバイス(Dave Abrahamsを含む)を、それが適用されるドメインを超えて、allstd::stringパラメーターに適用するために簡素化します-Always値でstd::stringを渡すことは、これらの講演/記事の最適化は適用に焦点を当てているため、任意のすべての任意のパラメーターおよびアプリケーションの「ベストプラクティス」限定されたケースのみ

値を返したり、パラメーターを変更したり、値を取得したりする場合、値を渡すことでコストのかかるコピーを節約し、構文上の利便性を提供できます。

いつものように、const参照を渡すと、コピーが大幅に節約されますコピーが不要な場合

次に、具体的な例を示します。

ただし、invalはまだ参照のサイズ(通常はポインターとして実装されています)よりもかなり大きいです。これは、std :: stringには、ヒープへのポインターや短い文字列最適化のためのメンバーchar []などのさまざまなコンポーネントがあるためです。ですから、参照渡しはまだ良い考えだと思います。ハーブがこれを言った理由を誰でも説明できますか?

スタックサイズが問題になる場合(およびインライン化/最適化されていない場合)、return_val + inval> return_val-IOW、ピークスタックの使用率は、ここで値を渡すことでreducedにすることができます(注:ABIの単純化)。一方、const参照を渡すと、最適化が無効になる可能性があります。ここでの主な理由は、スタックの成長を避けるためではなく、最適化を実行できるようにするためです適用可能な場合

Const参照による受け渡しの日は終わりません-ルールは以前よりも複雑になりました。パフォーマンスが重要な場合は、実装で使用する詳細に基づいて、これらの型をどのように渡すかを検討するのが賢明です。

150
justin

これはコンパイラの実装に大きく依存します。

しかし、それはあなたが何を使うかにもよります。

次の機能を考えてみましょう。

bool foo1( const std::string v )
{
  return v.empty();
}
bool foo2( const std::string & v )
{
  return v.empty();
}

インライン展開を避けるために、これらの関数は別のコンパイル単位で実装されています。その後:
1。これら2つの関数にリテラルを渡しても、パフォーマンスに大きな違いはありません。どちらの場合も、文字列オブジェクトを作成する必要があります。
2。別のstd :: stringオブジェクトを渡すと、foo2はディープコピーを実行するため、foo1foo1よりもパフォーマンスが優れています。

私のPC上で、g ++ 4.6.1を使用して、私はこれらの結果を得ました:

  • 参照による変数:1000000000反復 - >経過時間:2.25912秒
  • 値による変数:1000000000反復 - >経過時間:27.2259秒
  • 参照によるリテラル:100000000反復 - >経過時間:9.10319秒
  • 値によるリテラル:100000000反復 - >経過時間:8.62659秒
56
BЈовић

短い答え: いいえ! 長い答え:

  • 文字列を変更しない場合(treatは読み取り専用として)、const ref&として渡します。
    const ref&は、それを使用する関数が実行されている間、明らかにスコープ内に留まる必要があります)
  • 修正しようとしている場合、または範囲外になることがわかっている場合は、(threads)valueとして渡します。関数本体内にconst ref&をコピーしないでください。

cpp-next.com に投稿された "スピードが欲しい、値渡し"! 。 TL; DR

ガイドライン :関数の引数をコピーしないでください。代わりに、それらを値で渡して、コンパイラーにコピーさせてください。

^ の翻訳

関数の引数をコピーしないでください ---の意味:引数の値を内部変数にコピーして変更する場合は、代わりに値の引数を使用してください

だから、 しないでください

std::string function(const std::string& aString){
    auto vString(aString);
    vString.clear();
    return vString;
}

これを行う

std::string function(std::string aString){
    aString.clear();
    return aString;
}

関数本体の引数値を変更する必要があるとき.

関数本​​体の中で引数をどのように使うつもりなのかを知っておく必要があります。読み取り専用またはNOT ...そして範囲内であれば。

45
CodeAngry

あなたが実際にコピーを必要としない限り、それはconst &を取ることはまだ合理的です。例えば:

bool isprint(std::string const &s) {
    return all_of(begin(s),end(s),(bool(*)(char))isprint);
}

値で文字列を取るようにこれを変更すると、パラメータを移動またはコピーすることになります。その必要はありません。コピー/移動がより高価になる可能性があるだけでなく、新しい潜在的な障害も発生させます。既存の値への参照を取ることはできませんがコピー/移動は例外をスローすることができます(例えば、コピー中の割り当てが失敗する可能性があります)。

あなたが do コピーを必要とするならば、値による受け渡しは通常(常に?)最良の選択肢です。実際、C++ 03では、余分なコピーが実際にパフォーマンス上の問題を引き起こすことに気付かない限り、私は一般に心配しません。コピーエリジョンは現代のコンパイラではかなり信頼できるようです。私はあなたがRVOのためのあなたのコンパイラサポートのテーブルをチェックしなければならないという人々の懐疑論と主張は今日ほとんど時代遅れであると思います。


要するに、C++ 11は、コピーの排除を信頼していない人々を除いて、この点に関して実際には何も変更しません。

42
bames53

ほとんどです。

C++ 17には、basic_string_view<?>があります。これは、std::string const&パラメーターの基本的な1つの狭い使用例です。

移動セマンティクスの存在により、std::string const&のユースケースが1つなくなりました。パラメーターを格納することを計画している場合は、変数からmoveを取り出すことができるため、値によるstd::stringの取得がより最適です。

誰かがあなたの関数を生のC "string"で呼び出した場合、これはstd::stringの場合の2つではなく、1つのstd::string const&バッファしか割り当てられていないことを意味します。

しかし、コピーをするつもりがないのであれば、C++ 14ではstd::string const&による取得は依然として有用です。

std::string_viewを使用すると、Cスタイルの'\0'で終了する文字バッファを想定したAPIに上記の文字列を渡さない限り、割り当てを危険にさらすことなくstd::stringのような機能をより効率的に取得できます。生のC文字列は、割り当てや文字のコピーをしなくてもstd::string_viewに変換できます。

その時点で、std::string const&の用途は、データを大量にコピーしていない場合で、それをnullで終了するバッファを想定したCスタイルのAPIに渡す予定であり、std::stringが提供する高レベルの文字列関数が必要です。 。実際には、これはまれな要件のセットです。

std::stringPlain Old Data(POD) ではなく、その生のサイズはこれまでで最も重要なものではありません。たとえば、SSOの長さを超え、ヒープに割り当てられている文字列を渡すと、コピーコンストラクタはSSOストレージをコピーしないと予想されます。

これが推奨される理由は、invalが引数式から構築されているため、常に適切に移動またはコピーされるためです。引数の所有権が必要であれば、パフォーマンスが低下することはありません。そうでなければ、const参照はまだより良い方法です。

16
Puppy

この質問 から回答をコピー/貼り付けし、この質問に合うように名前とスペルを変更しました。

これは、求められていることを測定するためのコードです。

#include <iostream>

struct string
{
    string() {}
    string(const string&) {std::cout << "string(const string&)\n";}
    string& operator=(const string&) {std::cout << "string& operator=(const string&)\n";return *this;}
#if (__has_feature(cxx_rvalue_references))
    string(string&&) {std::cout << "string(string&&)\n";}
    string& operator=(string&&) {std::cout << "string& operator=(string&&)\n";return *this;}
#endif

};

#if PROCESS == 1

string
do_something(string inval)
{
    // do stuff
    return inval;
}

#Elif PROCESS == 2

string
do_something(const string& inval)
{
    string return_val = inval;
    // do stuff
    return return_val; 
}

#if (__has_feature(cxx_rvalue_references))

string
do_something(string&& inval)
{
    // do stuff
    return std::move(inval);
}

#endif

#endif

string source() {return string();}

int main()
{
    std::cout << "do_something with lvalue:\n\n";
    string x;
    string t = do_something(x);
#if (__has_feature(cxx_rvalue_references))
    std::cout << "\ndo_something with xvalue:\n\n";
    string u = do_something(std::move(x));
#endif
    std::cout << "\ndo_something with prvalue:\n\n";
    string v = do_something(source());
}

私にとってこれは出力:

$ clang++ -std=c++11 -stdlib=libc++ -DPROCESS=1 test.cpp
$ a.out
do_something with lvalue:

string(const string&)
string(string&&)

do_something with xvalue:

string(string&&)
string(string&&)

do_something with prvalue:

string(string&&)
$ clang++ -std=c++11 -stdlib=libc++ -DPROCESS=2 test.cpp
$ a.out
do_something with lvalue:

string(const string&)

do_something with xvalue:

string(string&&)

do_something with prvalue:

string(string&&)

以下の表は私の結果をまとめたものです(clang -std = c ++ 11を使用)。最初の数字はcopy constructionの数で、2番目の数字はmove構築の数です。

+----+--------+--------+---------+
|    | lvalue | xvalue | prvalue |
+----+--------+--------+---------+
| p1 |  1/1   |  0/2   |   0/1   |
+----+--------+--------+---------+
| p2 |  1/0   |  0/1   |   0/1   |
+----+--------+--------+---------+

値渡しソリューションでは、オーバーロードが1回だけで済みますが、左辺値とx値を渡すときに余分な移動構造が必要になります。これは与えられた状況のために受け入れられるかもしれないし、受け入れられないかもしれません。どちらの方法にも長所と短所があります。

16
Howard Hinnant

Herb Sutterは、Bjarne Stroustroupと一緒に、const std::string&をパラメータタイプとして推奨することで、まだ記録に残っています。 https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Rf-in を参照してください。

ここで他の答えのどれにも言及されていない落とし穴があります:あなたがconst std::string&パラメータに文字列リテラルを渡すならば、それはリテラルの文字を保持するためにその場で作成される一時的な文字列への参照を渡します。その参照を保存した場合、一時的な文字列の割り当てが解除されると無効になります。安全のために、参照ではなく copy を保存する必要があります。問題は、文字列リテラルがconst char[N]型であり、std::stringへの昇格が必要であるという事実から生じています。

以下のコードは、落とし穴とその回避策、およびマイナーな効率化オプション、 C++で参照として文字列リテラルを渡す方法はあります で説明されているconst char*メソッドによるオーバーロードを示しています。

(注:Sutter&Stroustroupは、文字列のコピーを保存する場合は、&&パラメータとstd :: move()を使ってオーバーロードされた関数も提供することをお勧めします。)

#include <string>
#include <iostream>
class WidgetBadRef {
public:
    WidgetBadRef(const std::string& s) : myStrRef(s)  // copy the reference...
    {}

    const std::string& myStrRef;    // might be a reference to a temporary (oops!)
};

class WidgetSafeCopy {
public:
    WidgetSafeCopy(const std::string& s) : myStrCopy(s)
            // constructor for string references; copy the string
    {std::cout << "const std::string& constructor\n";}

    WidgetSafeCopy(const char* cs) : myStrCopy(cs)
            // constructor for string literals (and char arrays);
            // for minor efficiency only;
            // create the std::string directly from the chars
    {std::cout << "const char * constructor\n";}

    const std::string myStrCopy;    // save a copy, not a reference!
};

int main() {
    WidgetBadRef w1("First string");
    WidgetSafeCopy w2("Second string"); // uses the const char* constructor, no temp string
    WidgetSafeCopy w3(w2.myStrCopy);    // uses the String reference constructor
    std::cout << w1.myStrRef << "\n";   // garbage out
    std::cout << w2.myStrCopy << "\n";  // OK
    std::cout << w3.myStrCopy << "\n";  // OK
}

出力:

const char * constructor
const std::string& constructor

Second string
Second string
13
circlepi314

std::stringにC++参照を使用するIMOは迅速で短いローカル最適化ですが、値渡しを使用することはより良いグローバル最適化になる可能性があります。

だから答えは:それは状況によります:

  1. 外側から内側の関数まですべてのコードを書く場合、そのコードが何をするのか知っているので、参照const std::string &を使うことができます。
  2. ライブラリコードを書く場合や、文字列が渡される場所でライブラリコードを多用する場合は、std::stringコピーコンストラクタの動作を信頼することで、グローバルな意味でより多くのことが得られるでしょう。
7

「Herb Sutter」の「基本に戻る!現代のC++スタイルの要点」 。他のトピックの中でも、彼は過去に与えられたパラメータ引き渡しアドバイス、およびC++ 11およびC++に伴う新しいアイデアをレビューします。特に文字列を値で渡すという考え方に注目します。

slide 24

ベンチマークは、std::stringsを値で渡すこと(関数がとにかくコピーする場合)がかなり遅くなる可能性があることを示しています。

これは、const&バージョンでは、すでに割り当てられているバッファを再利用する可能性があるため、常にフルコピーを作成してから(その場で移動するように)強制するためです。

彼のスライド27を見てください。「セット」機能のために、オプション1はいつものように同じです。オプション2は右辺値参照に過負荷を追加しますが、複数のパラメーターがある場合、これは組み合わせ爆発を引き起こします。

値渡しのテクニックが有効なのは、文字列を作成する必要がある(既存の値を変更していない)「シンク」パラメータに対してのみです。つまり、 コンストラクタ で、パラメータは一致する型のメンバを直接初期化します。

あなたがこれについてどれだけ深く心配できるかを知りたいなら、 Nicolai Josuttisの presentationを見て、幸運を祈ってください( “ Perfect - Done!” 以前のバージョンでの不具合を発見してn回)。行ったことがある?)


これは標準ガイドラインでも F.15 としてまとめられています。

5
JDługosz

@JDługoszがコメントで指摘しているように、Herbは別の(後の?)講演で他のアドバイスをします、ここから大まかに見てください: https://youtu.be/xnqTKD8uD64?t=54m50s

彼のアドバイスは、いわゆるシンク引数を取る関数fに値パラメータを使用することに限られています。これらのシンク引数から構造体を移動すると仮定します。

この一般的な方法では、それぞれlvalue引数とrvalue引数に合わせて最適化されたfと比較して、lvalue引数とrvalue引数の両方に対してmoveコンストラクタのオーバーヘッドが追加されるだけです。その理由を確かめるために、fがvalueパラメーターを取るとします。ここで、Tはコピーおよび移動が可能な構築型です。

void f(T x) {
  T y{std::move(x)};
}

Lvalue引数を指定してfを呼び出すと、xを作成するためにコピーコンストラクターが呼び出され、yを作成するために移動コンストラクターが呼び出されます。一方、rvalue引数を指定してfを呼び出すと、moveコンストラクターが呼び出されてxが構築され、別のmoveコンストラクターが呼び出されてyが構築されます。

一般に、左辺値引数に対するfの最適な実装は以下のとおりです。

void f(const T& x) {
  T y{x};
}

この場合、yを構築するために1つのコピーコンストラクタだけが呼び出されます。右辺値の引数に対するfの最適な実装は、やはり一般的に次のとおりです。

void f(T&& x) {
  T y{std::move(x)};
}

この場合、yを構築するために1つの移動コンストラクタだけが呼び出されます。

したがって、賢明な妥協案は、valueパラメータを取り、最適な実装に関してlvalueまたはrvalueの引数を1つ追加のmoveコンストラクタで呼び出すことです。これは、Herbの講演でも説明されています。

@JDługoszがコメントで指摘したように、値渡しは、引数sinkからオブジェクトを構成する関数に対してのみ意味があります。引数をコピーする関数fがある場合、値渡しの方法は一般的な定数参照渡しの方法よりもオーバーヘッドが大きくなります。パラメータのコピーを保持する関数fに対する値渡しのアプローチは、次の形式になります。

void f(T x) {
  T y{...};
  ...
  y = std::move(x);
}

この場合、左辺値引数に対するコピー構成と移動代入、および右辺値引数に対する移動構成と移動代入があります。左辺値引数に対する最も最適なケースは、次のとおりです。

void f(const T& x) {
  T y{...};
  ...
  y = x;
}

これは代入のみになります。これは、コピーコンストラクタと値渡しアプローチに必要な移動代入よりもはるかに安価です。その理由は、コピーコンストラクタが通常メモリを割り当てるのに対し、代入はy内の既存の割り当てられたメモリを再利用して、割り当てを防ぐ(de)ことができるからです。

右辺値引数について、コピーを保持するfの最も最適な実装は以下の形式を取ります。

void f(T&& x) {
  T y{...};
  ...
  y = std::move(x);
}

したがって、この場合は移動割り当てのみです。 const参照をとるfのバージョンに右辺値を渡すと、移動代入ではなく代入のみがかかります。したがって、一般的な実装として、この場合はconst参照を使用するバージョンのfが推奨されます。

そのため、一般に、最も最適な実装のためには、トークで示されているように、過負荷になるか、ある種の完璧な転送を行う必要があります。欠点は、引数の値カテゴリでオーバーロードを選択した場合に必要なオーバーロードの数の組み合わせが急増することです。これは、_ fのパラメーターの数によって異なります。完全転送にはfがテンプレート関数になるという欠点があります。これにより仮想化できなくなり、100%正しくしたい場合はかなり複雑なコードになります(詳細についてはトークを参照してください)。

2

問題は、 "const"は非詳細な修飾子であるということです。通常「const string ref」が意味するのは、「参照文字列を変更しない」ではなく、「この文字列を変更しない」ということです。 C++では、whichメンバーを「const」と言うことはできません。それらはすべて存在するか、存在しないかのいずれかです。

この言語の問題を回避するために、STL couldはあなたの例の中で "C()"が移動意味コピーとにかくを作成することを許可し、参照カウントまで(可変)明確に指定されている限り、これで問題ありません。

STLにはないので、参照カウンタをconst_cast <>離れた文字列のバージョンがあります(クラス階層内で何かを遡及的に変更可能にする方法はありません)。そして、cmstringをconst参照として自由に渡すことができます。一日中、リークや問題のない、深い機能でそれらのコピーを作成します。

ここではC++では「派生クラスの定数の細分性」は提供されていないため、適切な仕様を作成し、光沢のある新しい「定数移動可能文字列」(cmstring)オブジェクトを作成するのが最善の解決策です。

1
Erik Aronesty