標準イテレータの範囲が[開始、終了]ではなく[開始、終了]なのはなぜですか?


204

なぜ規格end()は実際の終わりではなく、終わりを過ぎたものとして定義するのですか?


19
「それは標準が言っていることだから」とそれをカットしないと思いますよね?:)
Luchian Grigore

39
@LuchianGrigore:もちろん違います。それは私たちの基準(背後にいる人々)に対する私たちの尊敬を損ないます。標準によって行われる選択には理由があることを期待する必要があります。
Kerrek SB

4
要するに、コンピューターは人間のように数えられません。しかし、人々がコンピュータのように数えられない理由に興味があるなら、私は「Nothing that is:A Natural History of Zero」を人間が発見した問題が1つ少ないことを発見した問題を詳細に調べることをお勧めします1つより。
ジョンマクファーレン

8
「最後の1つ」を生成する方法は1つしかないため、本物でなければならないため、多くの場合、安くはありません。「あなたは崖の端から落ちた」と生成することは常に安く、多くの可能な表現が行うでしょう。(void *) "ahhhhhhh"で問題ありません。
ハンスパッサント

6
質問の日付を見て、しばらくの間、あなたは冗談だと思っていました。
Asaf

回答:


286

最良の議論は簡単にダイクストラ自身が作ったものです:

  • 範囲のサイズを単純な差にしたい場合end  -  begin ;

  • シーケンスを空のシーケンスに退化する場合、下限を含めることはより「自然」です。また、代替(下限を除く)には「1つ前の」センチネル値の存在が必要になるためです。

なぜ1ではなく0からカウントを開始するのかを正当化する必要がありますが、それは質問の一部ではありませんでした。

[begin、end)規則の背後にある知恵は、自然に連鎖する範囲ベースの構造に対する複数のネストされた呼び出しまたは反復された呼び出しを処理するアルゴリズムの種類がある場合に、何度も報われます。対照的に、2重に閉じた範囲を使用すると、1つずつずれ、非常に不快でノイズの多いコードが発生します。たとえば、パーティション[ n 0n 1)[ n 1n 2)[ n 2n 3)をます。別の例は、標準的な反復ループfor (it = begin; it != end; ++it)であり、これはend - begin時間を実行します。両端が含まれている場合、対応するコードは読みにくくなります。空の範囲を処理する方法を想像してみてください。

最後に、カウントをゼロから開始する必要がある理由を説明することもできます。N要素の範囲が指定されている場合(たとえば、配列のメンバーを列挙する場合)、先ほど確立した範囲のハーフオープン規則を使用すると、 0は自然な「始まり」であるため、範囲を[0、N)と書くことができ、厄介なオフセットや修正はありません。

簡単に言うと1、範囲ベースのアルゴリズムのどこにも数字が表示されないという事実は、[開始、終了)規約の直接の結果であり、その動機です。


2
サイズNの配列を反復処理する一般的なC forループは、「for(i = 0; i <N; i ++)a [i] = 0;」です。今、それをイテレータで直接表現することはできません-多くの人々は<を意味のあるものにするために時間を無駄にしました。しかし、「for(i = 0; i!= N; i ++)...」と言ってもほぼ同じように明白です。したがって、0を開始し、Nを終了するのが便利です。
Krazy Glew

3
@KrazyGlew:ループの例に意図的に型を入れていません。あなたが考える場合beginendとしてint値を持つの0N、それぞれ、それが完全に適合しています。おそらく、これ!=は従来の<に比べてより自然な状態ですが、より一般的なコレクションについて考え始めるまで、そのことを発見することはできませんでした。
Kerrek SB 2012

4
@KerrekSB:私は、「より一般的なコレクションについて考え始めるまで、[!=の方が良い]を発見したことはなかった」ことに同意します。これは、Stepanovの功績の1つです。STLの前にそのようなテンプレートライブラリを作成しようとした人物と言えます。ただし、 "!="の方がより自然であると主張します。あるいは、!=がおそらくバグを導入していることを主張します。for(i = 0; i!= 100; i + = 3)...
Krazy Glew、

@KrazyGlew:シーケンス{0、3、6、...、99}はOPが要求した形式ではないため、最後のポイントはやや話題から外れています。このようにしたい場合は、++-incrementableイテレータテンプレートを作成する必要があります。このテンプレートstep_by<3>には、最初にアドバタイズされたセマンティクスが含まれます。
Kerrek SB、2012

@KrazyGlew <がバグを隠すことがある場合でも、とにかくバグです。誰かがを使用!=する必要があるときに使用する場合<それはバグです。ちなみに、このエラーの王は、単体テストやアサーションで簡単に見つけることができます。
Phil1970

80

実際、イテレータがシーケンスの要素を指しているのはなくのにあり、次の要素へのアクセスを逆参照することを考えると、イテレータに関連する多くの要素が突然、はるかに意味をなすようになります。次に、「過去の終わり」のイテレータが突然意味をなすようになります。

   +---+---+---+---+
   | A | B | C | D |
   +---+---+---+---+
   ^               ^
   |               |
 begin            end

明らかにbeginシーケンスの始まりをend指し、同じシーケンスの終わりを指します。逆参照beginは要素Aにアクセスしますが、逆参照endはそれに適切な要素がないため意味がありません。また、i途中にイテレータを追加すると、

   +---+---+---+---+
   | A | B | C | D |
   +---+---+---+---+
   ^       ^       ^
   |       |       |
 begin     i      end

あなたはすぐにから要素の範囲ことを見るbeginにはi要素が含まれているABの要素の範囲ながらiend要素が含まれていますCD。逆参照iすると、要素の権利が与えられます。つまり、2番目のシーケンスの最初の要素です。

逆反復子の「オフバイワン」でさえ、突然そのように明らかになります。その順序を逆にすると、次のようになります。

   +---+---+---+---+
   | D | C | B | A |
   +---+---+---+---+
   ^       ^       ^
   |       |       |
rbegin     ri     rend
 (end)    (i)   (begin)

以下の括弧内に、対応する非リバース(ベース)イテレーターを書きました。ご覧のとおり、i(私がと命名したri)に属する逆反復子は、まだ要素Bとの間にありCます。ただし、順序を逆にしたため、要素Bはその右側にあります。


2
私は数字で指摘イテレータ、および要素は数字(構文の間であった場合、それがより良い示すかもしれないと思うが、これは、最良の答え私見でfoo[i]すぐにアイテムの省略形です)の後の位置i)。考えてみると、「位置iの直後の項目」と「位置iの直前の項目」に別々の演算子を使用すると、多くのアルゴリズムが隣接する項目のペアを処理し、「ポジションiのいずれかの側のアイテムは、「ポジションiおよびi + 1のアイテム」よりもクリーンな場合があります。
スーパーキャット2015年

@supercat:数字は反復子の位置/インデックスを示すものではなく、要素自体を示すものでした。わかりやすくするために、数字を文字に置き換えます。実際、私の例のシーケンスには要素がないため、begin[0](ランダムアクセスイテレータを想定して)指定された数値でelement 1にアクセスします0
celtschk

「開始」ではなく「開始」という単語が使用されるのはなぜですか?結局のところ、「begin」は動詞です。
user1741137

@ user1741137「begin」は「beginning」の略語であることを意図していると思います(今では意味があります)。"beginning"が長すぎると、 "beginning"はぴったりと聞こえます。"start"は動詞 "start"と競合します(たとえばstart()、特定のプロセスを開始するためにクラスで関数を定義する必要がある場合など、既存のプロセスと競合すると煩わしくなります)。
Fareanor

74

なぜ規格end()は実際の終わりではなく、終わりを過ぎたものとして定義するのですか?

なぜなら:

  1. 空の範囲の特別な処理を回避します。空の範囲のbegin()場合、end()& と等しい
  2. これにより、要素を反復するループの終了基準が単純になります。ループend()は、到達しない限り継続するだけです。

64

それで

size() == end() - begin()   // For iterators for whom subtraction is valid

あなたは厄介なことをする必要はありません

// Never mind that this is INVALID for input iterators...
bool empty() { return begin() == end() + 1; }

あなたは誤って次のような誤ったコードを書くことはありません

bool empty() { return begin() == end() - 1; }    // a typo from the first version
                                                 // of this post
                                                 // (see, it really is confusing)

bool empty() { return end() - begin() == -1; }   // Signed/unsigned mismatch
// Plus the fact that subtracting is also invalid for many iterators

また、有効な要素をポイントすると何がfind()返されend()ますか?
あなたがでください本当にしたい他のメンバーと呼ばれるinvalid()無効なイテレータを返します!
2つのイテレータはすでに十分に苦痛です...

ああ、そしてこの関連記事見てください


また:

endが最後の要素の前にあった場合、どのようにinsert()して真の終わりを迎えますか?!


2
これは非常に過小評価されている答えです。例は簡潔で要点を正しており、「また」は他の誰からも言われず、振り返ってみると非常に明白に思えるが、啓示のように私を襲ったようなものです。
underscore_d

@underscore_d:ありがとう!! :)
user541686

ところで、私が賛成投票をしないという偽善者のように思える場合は、それは私がすでに2016年7月に戻っていたからです!
underscore_d

@underscore_d:気づかなかったけど、ありがとう!:)
user541686

22

半分閉じた範囲のイテレータイディオム[begin(), end())は、元はプレーン配列のポインター演算に基づいています。その操作モードでは、配列とサイズが渡された関数があります。

void func(int* array, size_t size)

[begin, end)その情報があれば、半分閉じた範囲への変換は非常に簡単です。

int* begin;
int* end = array + size;

for (int* it = begin; it < end; ++it) { ... }

完全に閉じた範囲で作業するには、より困難です。

int* begin;
int* end = array + size - 1;

for (int* it = begin; it <= end; ++it) { ... }

配列へのポインターはC ++の反復子であるため(そしてこれを可能にするように構文が設計されています)、を呼び出すstd::find(array, array + size, some_value)よりも呼び出す方がはるかに簡単std::find(array, array + size - 1, some_value)です。


プラス、あなたが半分閉じた範囲で動作している場合、あなたが使用することができます!=(あなたの事業者が正しく定義されている場合)becuase、終了条件をチェックする演算子を<意味します!=

for (int* it = begin; it != end; ++ it) { ... }

ただし、完全に閉じた範囲でこれを行う簡単な方法はありません。あなたは立ち往生してい<=ます。

C ++ でサポート<および>操作される唯一のイテレータは、ランダムアクセスイテレータです。<=C ++ですべてのイテレータークラスの演算子を作成する必要がある場合、すべてのイテレーターを完全に比較可能にする必要があり、能力の低いイテレーター(の双方向イテレーターstd::listや入力イテレーターなど)を作成するための選択肢が少なくなります。iostreamsC ++が完全に閉じた範囲を使用した場合、)で動作します。


8

ではend()最後過去ポインティング1は、forループでコレクションを反復処理するのは簡単です:

for (iterator it = collection.begin(); it != collection.end(); it++)
{
    DoStuff(*it);
}

end()最後の要素を指して、ループはより複雑になります:

iterator it = collection.begin();
while (!collection.empty())
{
    DoStuff(*it);

    if (it == collection.end())
        break;

    it++;
}

0
  1. コンテナが空の場合begin() == end()
  2. C ++プログラマーは、ループ条件の!=代わりに<(より少ない)を使用する傾向があるためend()、端から端までの位置を指すと便利です。
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.