一時的なものを受け入れる std::string_view:良いアイデアですか、それとも恐ろしい落とし穴ですか?

C++17 は std::string_view をもたらします .これは非常に便利なツールです:文字列を受け取るが、所有権を必要としない関数を書きたい場合、つまり view 、使用 std::string_view .両方の const char* をサポートします と std::string さらに、意図を明確に示しています。この関数はビューを取得します。何も所有せず、ビューするだけです。

正しい型を使用することを頻繁に提唱する者として、私は std::string_view について満足しています .しかし、議論が必要な設計上の決定が 1 つあります:std::string_view これは、ビューが一時的なものより長く存続する場合に問題を引き起こす可能性があります。これは、ビューが既に破棄されたデータを表示するようになったためです。

この決定の背後にある理由と、それが std::string_view を使用することの意味を調べてみましょう .

臨時受け入れの問題

std::string を格納するクラスを書いているとします。 、その文字列を取得する getter 関数を使用:

class foo
{
    std::string my_str_;

public:
    const std::string& get_str() const
    {
        return my_str_;
    }

    …
};

ゲッターは const で文字列を返します これで、std::string を使用していることがわかります。 後で別の文字列型に切り替えることにした場合は、 std::string でも 異なる種類のアロケーターを使用すると、API の変更である戻り値の型を変更する必要があります。

ただし、 std::string_view を使用できます ここでその問題を解決してください:

std::string_view get_str() const
{
    return my_str_;
}

char を格納する限り、任意の文字列実装を内部的に使用できるようになりました s は連続バッファにあり、ユーザーは気にする必要はありません。これが正しい抽象化と std::string_view の美点です。 .

ただし、foo に関する要件 リリースの少し前に、追加情報をその文字列に保存する必要があります。適切なリファクタリングを行うには、追加情報を追加します。おそらく、何らかのプレフィックス文字でしょうか? - 文字列へ。そして夜遅くに getter を素早く変更して、文字列全体ではなく部分文字列を返すようにします:

std::string_view get_str() const
{
    // substr starting at index 1 till the end
    return my_str_.substr(1u);
}

そのコードは機能すると思いますか?

さらに重要なこと:すべきと思いますか? 2 番目の答えは「間違いなく」です。文字列の一部でビューを作成しているだけですが、何が問題なのですか?

問題は std::string::substr() - ここで呼び出され、std::string を返します; 一時的 std::string .したがって、使用しようとするとすぐに爆発する一時オブジェクトへのビューを作成しています.

正しい解決策には、std::string_view への明示的な変換が必要です 最初:

std::string_view get_str() const
{
    return std::string_view(my_str_).substr(1u);
}

substr() の表示バージョン ここで正しいビューが返され、問題はありません。しかし、これは非常に微妙な変更であり、直感的ではありません。

ここでの主な問題は、std::string::substr() の戻り値の型です。 、 std::string_view に変更する必要があります .そしてこれは、C++ では解決されないダングリング参照の一般的な問題の 1 つの側面でもあります。

しかし、この場合、それを防ぐのは非常に簡単だったかもしれません.If std::string_view 一時的なものではなく、左辺値のみを受け入れるため、問題のあるコードはコンパイルされません。これでもダングリング参照は許可されますが、これらのような愚かな間違いを防ぐことができます。また、エラーを 1 つだけ防いだとしても、エラーをまったく防げないよりはましです。 /P>

では、なぜ std::string_view は 一時的なものを許可しますか?

標準化委員会の人々は愚かではありません、彼らは std::string_view を知っていました また、std::string_view を防ぐ方法も知っていました。 一時的な受け入れから.

では、その決定の背後にある理由は何ですか?

答えは std::string_view の最大のユースケースです :

臨時雇用を受け入れるメリット

std::string_view 所有していない文字列パラメータに最適です :

void do_sth(std::string_view str);

const char* を取る関数 または const std::string& std::string_view を使用するように更新する必要があります .

std::string_view を使用する場合 関数パラメーターとして、一時的な問題が発生することはありません:

do_sth(std::string("hi").substr(1u));

ここでは、完全な式の最後で破棄される一時的なものをまだ渡していますが、それが起こったとき、関数呼び出しはすでに終わっています!関数がビューをどこかにコピーしない限り、問題はありません.

さらに、一時的な受け入れは機能しているだけでなく、望まれている :

std::string get_a_temporary_string();
…
do_sth(get_a_temporary_string());

std::string_view の場合 一時的なものを受け入れていなかった場合は、次を使用する必要があります:

auto tmp = get_a_temporary_string();
do_sth(tmp);

そして、それは冗長すぎるかもしれません.

誰が std::string_view を使ったのですか?

ガイドライン

std::string_view を使用しても完全に安全です 関数が文字列の非所有ビューを必要とし、そのビューを別の場所に保存する必要がない場合は、関数パラメーターで。

std::string_view を使用するときは注意してください 関数が一時的な値を返さないようにしてください。std::string::substr() を呼び出すときは注意してください。 .

std::string_view を保存するときは十分に注意してください どこか、つまりクラス オブジェクト内。表示された文字列がビューより長く存続することを確認してください。

std::string_view を避けることを検討してください ローカル変数の型として、auto&& を使用します

最後のポイントについては話しませんでした:一部の関数でローカルにビューを作成することが望ましい場合があります。そこでも、ダングリング参照の問題に遭遇する可能性があります。代わりに実際の参照を使用する場合、有効期間の延長により一時十分に長生きしてください。これは std::string_view です

このガイドラインは妥当に思えますが、私は満足していません.そのガイドラインには「注意」が多すぎます.C++ はすでに十分に複雑です.これ以上複雑にしないようにしましょう.

そして、より良い解決策があります:私の古くからの友人である型システムを使用してください。

function_view vs function_ref

しばらく前にヴィットリオ・ロメオが function_view についての投稿を公開しました implementation.function_view std::string_view です std::function に相当 .そして std::string_view のように template <typename Functor> void do_sth(data_t data, Functor callback) の代替として設計されたため、一時的なものを受け入れました。

テンプレート パラメーターを介してコールバックを渡す代わりに、function_view 代わりに使用できます。指定された署名を持つすべての関数を許可します。

彼が実装を書いた頃、私は object_ref に取り組んでいました。 私の type_safe ライブラリの object_ref 基本的に非 null ポインターです。現在は object_ref です。 永続的な参照を格納するためのものです。つまり、クラス内のメンバーとして、右辺値を受け入れるべきではありません。結局のところ、一時的な参照も指すことはできません。

それで、Vittorio の投稿を読んで、「一時的なものを受け入れるべきではない」と判断したとき、function_view を書きました。 function_ref と呼んでいます。 object_ref と一致するように function_view としてブログに書きました 一時的なものを受け入れないことは、あなたが思っているよりも難しいことです.

投稿後、reddit で議論がありました。彼らは - 正しく - 一時変数を受け入れないことが関数パラメータとして使用するのを難しくしていると指摘しました.

そして、それは私を襲った:function_view および function_ref は 2 つの直交するものです!function_view 関数パラメータ function_ref 用に設計されています 他のすべてのために設計されています.function_view これは便利であり、関数パラメーターにとって安全であるため、一時を受け入れる必要があります function_ref

ビューと参照タイプ

パラメーターとしての非所有参照は、他の場所で使用される非所有参照とは異なるセマンティクスを必要とするため、そのために 2 つの別個の型を作成することは理にかなっています。

1 つのタイプ - ビュー - パラメータ用に設計されています。一時変数を受け入れる必要があります。通常の const T& ビュー タイプとしても適しています。

もう 1 つ - ref - 他のユース ケース用に設計されています。一時的なものは受け入れません。さらに、コンストラクタは explicit にする必要があります。 、あなたが長期的な参照を作成しているという事実を強調するために:

view_string(str);
refer_to_string(string_ref(str));
transfer_string(std::move(str));

これで、呼び出しサイトで各関数が何をするのか、寿命についてどこに注意する必要があるのか​​が明確になりました。

ポインターは、一時オブジェクトにバインドせず、作成時に明示的な構文 (&str ただし、null になる可能性があるため、これはオプションの ref 型です。非 const 左辺値参照はほとんど ref 型と見なされますが、欠けているのはそれを作成するための明示的な構文だけです。

XXX_view と名付けました と XXX_ref ですが、実際の名前は重要ではありません。重要なのは、洗練されたガイドラインを提案できることです:

ガイドライン

何かへの非所有参照が必要な場合は、ビューまたは参照型を使用してください。

ビュー タイプは、ビューが他の場所に保存されていない関数パラメーターとしてのみ使用します。ビュー タイプは短期間しか存続しません。

戻り値やオブジェクトへの格納など、他のすべてに ref 型を使用します。また、ref が別の場所に格納される関数パラメーターとして ref 型を使用し、呼び出し元は有効期間が機能することを確認する必要があります。

ref 型を使用する場合は、ポインターを使用する場合と同様に、有効期間に注意する必要があります。

結論

標準ライブラリは std::string_ref を提供していません 意図したセマンティクスで、おそらく今追加するには遅すぎます.したがって、コンパイラは思い出せないので、最初のガイドラインに従って一時的に注意する必要があります.

ただし、配列、関数など、他の多くのものを表示または参照できます。したがって、独自のビュー タイプを設計するときは、対応する参照タイプも提供することを検討してください。唯一の違いはコンストラクターにあるため、実装を簡単に共有できます。 .

しかし、多くのタイプでは、特別なビュー タイプは必要ありません。const T& 1 つのタイプだけを表示する必要がある場合は、ts::object_ref を使用できます。 、 gsl::non_null または単に T* 通常のオブジェクトの参照型として。

最後のガイドラインでは、関数パラメーターの 1 つのケースのみを対象としています:関数に単純に渡されるパラメーター。他の 2 つのケースは、入力パラメーターと出力パラメーターです。入力パラメーターについては、const T& で値渡しまたはオーバーロードを使用します。 と T&& .しかし、出力パラメータはどうすればよいのでしょうか?このブログ投稿でも説明されています。