1. ホーム
  2. c++

[解決済み】カスタムタイプを「範囲ベースのforループ」で動作させる方法とは?

2022-03-29 07:25:46

質問

最近、多くの人と同じように、私もC++11がもたらす様々な機能を試しています。私のお気に入りの1つは、「範囲ベースのforループ」です。

それはわかります。

for(Type& v : a) { ... }

と同等である。

for(auto iv = begin(a); iv != end(a); ++iv)
{
  Type& v = *iv;
  ...
}

そして、その begin() を返すだけです。 a.begin() は標準的なコンテナの場合です。

しかし、もし私が カスタムタイプのquot;範囲ベースのfor loop(quot;ループ)を意識させる。 ?

を特殊化すればよいのでしょうか? begin()end() ?

カスタムタイプが名前空間に属している場合 xml を定義する必要があります。 xml::begin() または std::begin() ?

要するに、そのための指針は何なのか。

どのように解決するのですか?

質問(とほとんどの回答)が投稿された後に、規格が変更されました この不具合報告の解決に .

を作る方法です。 for(:) ループは、あなたのタイプで動作します。 X は、現在2つの方法のどちらかです。

  • メンバー作成 X::begin()X::end() は、イテレータのように動作するものを返します。

  • 自由な関数を作成する begin(X&)end(X&) と同じ名前空間で、イテレータのように動作するものを返すものです。 X

また、同様のことを const のバリエーションがあります。 これは、不具合報告の変更を実装しているコンパイラでも、そうでないコンパイラでも有効です。

返されるオブジェクトは、実際にイテレータである必要はありません。 そのため for(:) ループは、C++標準のほとんどの部分とは異なり と同等のものに展開するよう指定されています。 :

for( range_declaration : range_expression )

になります。

{
  auto && __range = range_expression ;
  for (auto __begin = begin_expr,
            __end = end_expr;
            __begin != __end; ++__begin) {
    range_declaration = *__begin;
    loop_statement
  }
}

で始まる変数が __ は説明のためだけのものであり begin_exprend_expr を呼び出すマジックです。 begin / end

begin/end の戻り値に関する要件は単純である。をオーバーロードする必要があります。 ++ が有効であること、初期化式が有効であること、バイナリ != はブーリアンコンテキストで使用できるもの、単項は * 初期化できるものを返すもの range_declaration で、パブリックなデストラクタを公開します。

イテレータと互換性のない方法でこれを行うのは、おそらく悪い考えでしょう。将来のC++の反復は、そうすればあなたのコードを壊すことに比較的寛大になるかもしれません。

余談ですが、将来の標準規格の改定で end_expr とは異なる型を返すように begin_expr . これは,手書きのC言語ループと同程度の効率に最適化しやすい遅延終了評価(NULL終端検出など)を可能にするなどの利点があります.


¹ 注意事項 for(:) ループは、任意のテンポラリを auto&& 変数に格納し、それを lvalue として渡します。 一時的な値(あるいは他のr値)に対して反復処理をしているかどうかを検出することはできません。 for(:) ループを使用します。 n4527 の [stmt.ranged] 1.2-1.3 を参照してください。

² どちらかを呼び出します。 begin / end メソッド、または自由関数の ADL ルックアップのみ begin / end , または マジックで C スタイルの配列をサポートします。 なお std::begin が呼び出されない限り range_expression の型のオブジェクトを返します。 namespace std またはそれに依存している。


c++17 range-for 式が更新されました。

{
  auto && __range = range_expression ;
  auto __begin = begin_expr;
  auto __end = end_expr;
  for (;__begin != __end; ++__begin) {
    range_declaration = *__begin;
    loop_statement
  }
}

の種類で __begin__end は切り離されました。

これにより、end iteratorがbeginと同じ型でないことが許容されます。 終了イテレータの型は、quot;sentinel" のみサポートすることができます。 != をbeginイテレータタイプとする。

なぜこれが便利かというと、エンドイテレータが、"check your char* を指しているかどうかを確認するために '0' の場合 == を持つ char* . これにより、C++ の range-for 式は、ヌル文字で終端する char* バッファを使用します。

struct null_sentinal_t {
  template<class Rhs,
    std::enable_if_t<!std::is_same<Rhs, null_sentinal_t>{},int> =0
  >
  friend bool operator==(Rhs const& ptr, null_sentinal_t) {
    return !*ptr;
  }
  template<class Rhs,
    std::enable_if_t<!std::is_same<Rhs, null_sentinal_t>{},int> =0
  >
  friend bool operator!=(Rhs const& ptr, null_sentinal_t) {
    return !(ptr==null_sentinal_t{});
  }
  template<class Lhs,
    std::enable_if_t<!std::is_same<Lhs, null_sentinal_t>{},int> =0
  >
  friend bool operator==(null_sentinal_t, Lhs const& ptr) {
    return !*ptr;
  }
  template<class Lhs,
    std::enable_if_t<!std::is_same<Lhs, null_sentinal_t>{},int> =0
  >
  friend bool operator!=(null_sentinal_t, Lhs const& ptr) {
    return !(null_sentinal_t{}==ptr);
  }
  friend bool operator==(null_sentinal_t, null_sentinal_t) {
    return true;
  }
  friend bool operator!=(null_sentinal_t, null_sentinal_t) {
    return false;
  }
};

実例 のことです。

最小限のテストコードは

struct cstring {
  const char* ptr = 0;
  const char* begin() const { return ptr?ptr:""; }// return empty string if we are null
  null_sentinal_t end() const { return {}; }
};

cstring str{"abc"};
for (char c : str) {
    std::cout << c;
}
std::cout << "\n";


以下は簡単な例です。

namespace library_ns {
  struct some_struct_you_do_not_control {
    std::vector<int> data;
  };
}

あなたのコードです。

namespace library_ns {
  int* begin(some_struct_you_do_not_control& x){ return x.data.data(); }
  int* end(some_struct_you_do_not_control& x){ return x.data.data()+x.data.size(); }
  int const* cbegin(some_struct_you_do_not_control const& x){ return x.data.data(); }
  int* cend(some_struct_you_do_not_control const& x){ return x.data.data()+x.data.size(); }
  int const* begin(some_struct_you_do_not_control const& x){ return cbegin(x); }
  int const* end(some_struct_you_do_not_control const& x){ return cend(x); }
}

これは、制御していない型を拡張して反復可能にする方法の例です。

ここでは、ポインターをイテレータとして返すことで、フードの下にベクトルがあることを隠しています。

自分が持っている型には、メソッドを追加することができます。

struct egg {};
struct egg_carton {
  auto begin() { return eggs.begin(); }
  auto end() { return eggs.end(); }
  auto cbegin() const { return eggs.begin(); }
  auto cend() const { return eggs.end(); }
  auto begin() const { return eggs.begin(); }
  auto end() const { return eggs.end(); }
private:
  std::vector<egg> eggs;
};

を再利用しています。 vector のイテレータです。 私は auto は簡潔である。 c++11 もっと冗長にしないと。

以下は、簡単で汚い反復可能なレンジビューです。

template<class It>
struct range_t {
  It b, e;
  It begin() const { return b; }
  It end() const { return e; }
  std::size_t size() const { return end()-begin(); }
  bool empty() const { return begin()==end(); }
 
  range_t without_back( std::size_t n = 1 ) const {
    n = (std::min)(n, size());
    return {begin(), end()-n};
  }
  range_t without_front( std::size_t n = 1 ) const {
    n = (std::min)(n, size());
    return {begin()+n, end()};
  }
  decltype(auto) front() const { return *begin(); }
  decltype(auto) back() const { return *(std::prev(end())); }
};
template<class C>
auto make_range( C&& c ) {
  using std::begin; using std::end;
  return range_t{ begin(c), end(c) };
}

を使って c++17 テンプレートクラスの控除です。

std::vector<int> v{1,2,3,4,5};
for (auto x : make_range(v).without_front(2) ) {
  std::cout << x << "\n";
}

は 3 4 5 を表示し、最初の 2 はスキップします。