🕺

[C++] クラスにbeginを作ってみる

2024/12/26に公開4

初めに

zennでは初投稿になります!
業務や趣味でC++をよく使いますが、10年以上使っている今でもC++には驚かされます。
面白い使い方を一つ紹介します。

クラスにbeginを作ってみる

std::vector<int> data = {0,1,2};
for(auto& i: data){
  std::cout << i << std::endl;
}

このようなコードを書いていて、あるとき思いました。
for(auto& i: data){
これって、カスタムで作れるのかと。

実は、カスタムクラスで表現できます。

class Data{
private:
  std::vector<int> data_ = {0,1,2};
public:
  std::vector<int>::iterator begin() { // イテレータ型を返す
    return data_.begin();
  }
  std::vector<int>::iterator end() { // イテレータ型を返す
    return data_.end();
  }
}

int main(){
  Data testData;
  for(auto& i: testData){
    std::cout << i << std::endl;
  }
  return 0;
}
0
1
2

なぜこれが可能なのかというと、
for(auto i: data)
この実装は以下のように展開されます。

// for ( for-range-declaration : for-range-initializer ) statement
{
  auto && __range = for-range-initializer;
  for ( auto __begin = begin-expr, __end = end-expr;
        __begin != __end;
        ++__begin ) {
    for-range-declaration = *__begin;
    statement
  }
}

(https://cpprefjp.github.io/lang/cpp11/range_based_for.html) 抜粋

つまり、
T* begin()T* end()
を実装することで
for(auto& i: data){
を使うことができます。

ちょっと応用編

c/c++のメモリマップを利用して面白い使い方ができます。
よく、x,y,zの3次元の点をクラスにすると思いますが、これを例に出してみます。
一番下に注意があります。

class Point{
public:
  int x;
  int y;
  int z;
  Point(int x_, y_, z_): x(x_), y(y_), z(z_){}

  int* begin(){
    return &x;
  }
  int* end(){
    return &z + 1;
  }
};

int main(){
  Point p(1,2,3);
  for(auto& i: p){
    std::cout << i << std::endl;
  }
  return 0;
}
1
2
3

begin が返す &x が最初の要素のアドレスになり、
end は &z の次のアドレスを示すため、ループはこの範囲を辿って進んでいきます。

実は、C++の構造体やクラスの非静的メンバ変数は、定義された順番でメモリにオフセットを持つことが保証されています (C++11以降の仕様)。そのため、この例では x -> y -> z の順にアクセスすることが可能です。

ただし、メモリが「連続して配置される」ことまでは、言語仕様として明記されていません。この連続性は実行環境(ABI)の規約に依存します。多くの実行環境では連続性が保証されていますが、完全な移植性を求めるコードでは注意が必要です。

動作確認した環境

以下の環境で動作確認を行いました:

  • OS: MacOS Monterey 12.7.1 6
    コンパイラ: Apple Clang 14.0.0
  • 組み込み環境: M5Stack Basic Core S3
    開発環境: Arduino IDE 2.2.1
    使用したボード設定: "ESP32 Arduino" ライブラリ

環境依存について

たとえば、以下のケースで連続性が崩れることがあります:

  • 非標準的なABIを持つ環境
  • アライメント(メモリ境界)に関する規約が異なる環境
  • クラスが派生クラスや仮想関数を含む場合

したがって、このコードは簡易的なデモや学習目的としては有効ですが、汎用的な用途や移植性を重視するシステムで使うのは避けることを強くおすすめします。

最後別の内容になってしまいましたが、
beginを作る話でした!

おしまい

ユカイ工学テックブログ

Discussion

齊藤敦志齊藤敦志

データメンバが定義された順序でオフセット値を持つことや他のクラスとレイアウトが互換になる場合についての規定はありますが、連続することは言語仕様としては保証されていないはずです。

実行環境の規格 (ABI) で保証されているならそれをあてにすることもあるかもしれませんが、一般的には良い作法とは言えないと思います。

kikochankikochan

ご指摘ありがとうございます!確かに、データメンバが定義された順にオフセットを持つことは保証されているものの、メモリの連続性については言語仕様として保証されていないですね。この点、ご指摘の通りだと思います。

私としても、このコードを何かのシステムにそのまま組み込むのは非常におすすめできないやり方です。特に移植性が求められる場面では、実行環境に依存したコードは思わぬトラブルを引き起こす可能性があります。

ただ、学習目的やC++の特性を楽しむデモとしては面白い題材だと思いますので、引き続き楽しんでいただければ嬉しいです!記事にも補足を追加して、より正確な情報をお伝えできるように改善します。

yaito3014yaito3014

std::vector<T>beginend が返すイテレータが T* である、もしくは T* に変換出来るということは保証されていないと思います。

kikochankikochan

ありがとうございます!
std::vector<int>beginend が返すイテレータ型が int* である保証は全くありませんね。
加えて、元のコードでは data.begin() と記載していましたが、正しくは data_.begin() ですね。
std::vector<int>::iterator でした!凡ミス!

これらを踏まえ、以下のように修正いたしました:

  • data_.begin() および data_.end() を使用
  • std::vector<int>::iteratorを扱うように変更

記事を更新いたしましたので、ぜひご確認ください!