チュートリアル:C++20 の反復子センチネル

おそらく、C++20 が範囲を追加することを知っているでしょう。 18 の代わりに !

範囲はさらに多くのことを行います。とりわけ、末尾にイテレータを指定する新しい方法、つまりセンチネルを追加します。

モチベーション

null で終わる文字列を何らかのバッファにコピーするとします (最後の null ターミネータを除く)。問題ありません。すぐにループを記述します:

void do_sth(const char* str)
{
    std::vector<char> buffer;
    while (*str)
    {
        buffer.push_back(*str);
        ++str;
    }

    // continue doing something
}

24 をインクリメントし続けます ポインタを挿入し、ヌル文字に到達するまで文字を挿入します。簡単なことです。

ただし、これは生のループであり、特定の状況では不適切なスタイルと見なされます。代わりに、STL アルゴリズムを使用する必要があります。この場合は 37 .45 を使用 コードは次のようになります:

void do_sth(const char* str)
{
    std::vector<char> buffer;
    std::copy(str, str + std::strlen(str),
              std::back_inserter(buffer));

    // continue doing something
}

55 を渡します イテレータ範囲と使用 60 出力イテレータとして 78 を繰り返し呼び出します 、上記のコードと同じですが、範囲を指定する方法に注意してください:開始イテレータは 88 です 終了イテレータは 96 です 、これはヌル ターミネータへのポインタです。100 と言うことで 最後に、119 文字列を反復処理して末尾を見つける必要があります。1 つではなく 2 つのループで終わります!最初のループは末尾を見つけるためのもので、2 つ目のループはすべての文字をコピーするためのものです。最初のバージョンでは、両方のループを 1 つにまとめました。 、コピー中に終了をチェックすることによって。

アルゴリズムを使用して同じことを達成できますか?

問題

C++ のイテレーターは一般化されたポインターです。それを逆参照して現在の値を取得し、インクリメントして次の値に移動し、他のイテレーターと比較できます。その結果、範囲は 2 つのイテレーターで指定されます。範囲を反復するとき、最初の値を繰り返しインクリメントし、最後を過ぎた値と等しくなるまで繰り返します:

for (auto iter = begin; iter != end; ++iter)
{
    auto value = *iter;
    …
}

これは、サイズがわかっているコンテナでは問題なく機能しますが、null で終わる文字列では機能しません。イテレータ。

他の言語では、イテレータの定義が異なります。範囲はイテレータのペアではなく、単一のオブジェクトによって定義されます。現在の値を取得して進めることができますが、イテレータ自体にそれが完了したかどうかを尋ねることもできます。反復は次のようになります:

for (auto iter = begin; !iter.is_done(); iter.advance())
{
    auto value = iter.get();
    …
}

このような反復子の概念では、null で終わる文字列を反復処理するのは簡単です:

class zstring_iterator
{
public:
    bool is_done() const
    {
        return *cur_ == '\0';
    }

    char get() const
    {
        return *cur_;
    }

    void advance()
    {
        ++cur_;
    }

private:
    const char* cur_;
};

イテレータを他のイテレータの位置と比較するのではなく、完了したかどうかをイテレータに尋ねるため、 124 で行ったように null 文字をチェックするだけです。 上記のループ バージョンです。C++ イテレータでも同じことができるようにしたいと考えています。

ソリューション

「この反復子は最後ですか?」とつづると、 138 として 、null 文字のチェックを簡単に入れることができますが、143 と綴ります。 .どうにかして 154 を回す必要があります 163 に相当するものに 幸いなことに、それを行う方法があります:演算子のオーバーロードです。

175 の代わりに 他のイテレータ (186 この新しい「終了のみ」のイテレータは逆参照できません。できることは、それを「通常の」イテレータと比較することだけです。この等値チェックには、それが最後にあるかどうかの反復子。

C++20 標準ライブラリでは、このような終了のみの反復子は sentinel と呼ばれます。 .次のようになります:

class iterator
{
    // Some iterator, with *, ++, etc.
};

// We still want to be able to compare two iterators.
bool operator==(iterator lhs, iterator rhs);
bool operator!=(iterator lhs, iterator rhs);

// The special end-only iterator.
// It is usually an empty type, we don't actually need any objects.
// It's just there because `==` takes two parameters.
class sentinel {};

bool operator==(iterator iter, sentinel)
{
    return /* is iter done? */;
}
bool operator!=(iterator iter, sentinel)
{
    return /* is iter not done? */;
}

bool operator==(sentinel, iterator iter);
bool operator!=(sentinel, iterator iter);

null で終了する文字列の番兵は、実装が簡単になりました。 、それを変更する必要はありません。

// Empty type.
struct zstring_sentinel {};

// Are we done?
bool operator==(const char* str, zstring_sentinel)
{
    return *str == '\0';
}

// != and reversed operators not needed in C++20.

それだけです。必要なのはそれだけです。これで、コピー コードを次のように記述できます。

void do_sth(const char* str)
{
    std::vector<char> buffer;
    std::copy(str, zstring_sentinel{}, std::back_inserter(buffer));

    // continue doing something
}

203 を渡す代わりに 、sentinel タイプを指定します。内部的に、アルゴリズムには 213 をインクリメントするループがあります。 終了イテレータと等しくなるまで。この場合、終了イテレータはセンチネルであるため、225 を呼び出します。 null ターミネータに到達したかどうかをチェックします。2 つのループは必要ありません。

ただし… コンパイルされません。

おわかりのように、イテレータの概念については実際には何も変更していませんが、範囲を指定する方法を変更しました。以前は、同じ型の 2 つのイテレータを渡していましたが、今は渡していません。コード>237 最初の 2 つの引数が同じ型である必要があります。

新しいイテレータとセンチネルの範囲を展開するには、署名に多少の協力が必要です。

新しい C++20 範囲化アルゴリズムはそれを行ったので、 246 を呼び出す代わりに 252 を呼び出す必要があります :

void do_sth(const char* str)
{
    std::vector<char> buffer;
    std::ranges::copy(str, zstring_sentinel{},
                      std::back_inserter(buffer));

    // continue doing something
}

言語バージョン、範囲ベースの 265 に注意してください ループは、C++17 で既に適切な更新を受け取っているため、小さなヘルパーを使用して、範囲ベースの 278 を使用できます。 282 を繰り返すループ :

struct zstring_range
{
    const char* str;

    auto begin() const
    {
        // The begin is just the pointer to our string.
        return str;
    }
    auto end() const
    {
        // The end is a different type, the sentinel.
        return zstring_sentinel{};
    }
};

void do_sth(const char* str)
{
    std::vector<char> buffer;
    for (auto c : zstring_range(str))
        buffer.push_back(c);

    // continue doing something
}

結論

終了が固定位置ではなく何らかの動的条件である範囲がある場合は常に、代わりに反復子とセンチネルのペアを使用してください。

// Empty tag type.
struct sentinel {};

// Check whether the associated iterator is done.
bool operator==(iterator iter, sentinel);

それをサポートするために、既存のアルゴリズムに必要なのは、署名を変更することだけです

template <typename I>
void algorithm(I begin, I end);

template <typename I, typename S>
void algorithm(I begin, S end);

他の変更は必要ないため、既存のセンチネルがない場合でも、今すぐ変更を開始する必要があります。これにより、将来の範囲タイプに備えてコードが準備されます。

センチネルは終了イテレータの一般的な代替ではないことに注意してください。 296 のようなコンテナの場合 、最後は単なる既知の位置であり、センチネルを導入する必要はありません.これにより、エンドイテレータをデクリメントして後方に移動することができます.これは、センチネルでは本質的に不可能なことです.