最近多くの人と同じように、私も C++11 がもたらすさまざまな機能を試しています。私のお気に入りの 1 つは、「範囲ベースの for ループ」です。
という事は承知しています:
for(Type& v : a) { ... }
以下と同等です:
for(auto iv = begin(a); iv != end(a); ++iv)
{
Type& v = *iv;
...
}
そして、それはbegin()
単純にa.begin()
標準のコンテナに戻ります。
しかし、カスタム タイプを「範囲ベースの for ループ」対応にしたい場合はどうすればよいでしょうか?
begin()
専門的にやるしかないのでしょうかend()
?
カスタム タイプが名前空間に属している場合xml
、xml::begin()
または を定義する必要がありますかstd::begin()
?
簡単に言えば、それを実行するためのガイドラインは何ですか?
ベストアンサー1
質問(およびほとんどの回答)が投稿されてから基準は変更されましたこの欠陥報告の解決において。
for(:)
ループを自分の型で動作させる方法は、X
次の 2 つの方法のいずれかになります。
メンバーを作成し
X::begin()
、X::end()
イテレータのように動作するものを返す自由な関数を作成し
begin(X&)
、end(X&)
型と同じ名前空間でイテレータのように動作するものを返しますX
。¹
バリエーションについても同様ですconst
。これは、欠陥レポートの変更を実装するコンパイラと実装しないコンパイラの両方で機能します。
返されるオブジェクトは実際にはイテレータである必要はありません。for(:)
ループは、
for( range_declaration : range_expression )
C++標準のほとんどの部分とは異なり、同等のものに展開するように指定:
{
auto && __range = range_expression ;
for (auto __begin = begin_expr,
__end = end_expr;
__begin != __end; ++__begin) {
range_declaration = *__begin;
loop_statement
}
}
ここで、 で始まる変数は__
説明のみを目的としており、begin_expr
は/ .²end_expr
を呼び出す魔法です。begin
end
begin/end 戻り値に関する要件は単純です。pre- をオーバーロードし++
、初期化式が有効であること、!=
ブールコンテキストで使用できるバイナリであること、*
割り当て初期化できるものを返す単項であることrange_declaration
、およびパブリックデストラクタを公開していることを確認する必要があります。
イテレータと互換性のない方法でこれを行うのはおそらく悪い考えです。そうすると、C++ の将来のイテレーションでは、コードが壊れることに対して比較的無頓着になる可能性があります。
余談ですが、将来の標準の改訂により、end_expr
とは異なる型を返すことが許可される可能性がかなりbegin_expr
あります。これは、手書きの C ループと同じくらい効率的に最適化しやすい「遅延終了」評価 (ヌル終了の検出など) を許可するなど、他の同様の利点がある点で便利です。
for(:)
¹ ループは一時変数を格納し、それを左辺値として渡すことに注意してくださいauto&&
。一時変数 (またはその他の右辺値) を反復処理しているかどうかを検出することはできません。このようなオーバーロードはループによって呼び出されませんfor(:)
。n4527 の [stmt.ranged] 1.2-1.3 を参照してください。
² begin
/end
メソッド、または ADL 専用のフリー関数begin
/の検索end
、またはC スタイルの配列サポートのマジックのいずれかを呼び出します。 が同じ型のオブジェクトを返すか、同じ型に依存するstd::begin
場合を除いて、 は呼び出されないことに注意してください。range_expression
namespace std
でC++17 の範囲指定式が更新されました
{
auto && __range = range_expression ;
auto __begin = begin_expr;
auto __end = end_expr;
for (;__begin != __end; ++__begin) {
range_declaration = *__begin;
loop_statement
}
}
__begin
との型は__end
分離されました。
これにより、終了反復子が開始反復子と同じ型でなくてもよくなります。終了反復子の型は、!=
開始反復子の型でのみサポートされる「センチネル」にすることができます。
これがなぜ便利なのかを示す実際の例としては、の場合に、終了反復子が「 をチェックして、char*
が を指しているかどうかを確認してください'0'
」と読み取ることができることが挙げられます。これにより、C++ の範囲 for 式は、null で終了するバッファーを反復処理するときに最適なコードを生成できます。==
char*
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
// C++20 only line: (off C++20 it generates a hard error)
requires std::random_access_iterator<It>
{
return end()-begin(); // do not use distance: O(n) size() is toxic
}
bool empty() const { return begin()==end(); }
range_t without_back() const {
if(emptty()) return *this;
return {begin(), std::prev(end())};
}
range_t without_back( std::size_t n ) const
// C++20 only line: (see below)
requires !std::random_access_iterator<It>
{
auto r=*this;
while(n-->0 && !r.empty())
r=r.without_back();
return r;
}
range_t without_front() const {
if(empty()) return *this;
return {std::next(begin()), end()};
}
range_t without_front( std::size_t n ) const
// C++20 only line: (see below)
requires !std::random_access_iterator<It>
{
auto r=*this;
while(n-->0 && !r.empty())
r=r.without_front();
return r;
}
// C++20 section:
range_t without_back( std::size_t n ) const
requires std::random_access_iterator<It>
{
n = (std::min)(n, size());
return {b, e-n};
}
range_t without_front( std::size_t n ) const
requires std::random_access_iterator<It>
{
n = (std::min)(n, size());
return {b+n, e};
}
// end C++20 section
decltype(auto) front() const { return *begin(); }
decltype(auto) back() const { return *(std::prev(end())); }
};
template<class It>
range_t(It,It)->range_t<It>;
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";
}
最初の 2 をスキップして、3、4、5 を出力します。