LoginSignup
3
2

More than 1 year has passed since last update.

C++20の<span>で安全に配列アクセス

Last updated at Posted at 2022-07-11

はじめに

  • ライブラリから配列が渡された場合、オーバーフローさせない自信はありますか?

標準ライブラリ <span>

spanという標準ライブラリがC++20から追加されました。Cタイプの生配列にありがちな、オーバーフローなどのアクセスバグを排除できるライブラリです。
次のように生配列へのポインタとサイズを渡すことで、vectorのようにサイズを意識することなくアクセスできます。

#include <iostream>
#include <span>
using namespace std;
int main(int argc, char* argv[])
{
	span<char*> appArgs(argv, argc);
	for (auto arg: appArgs)
		cout << arg << endl;
}

どんなときに使えばいい?

  • <span>が有効なのは、配列のポインタがライブラリから渡された場合です。
  • まず、基本的な考えとして<span>は使わなくて済むような実装しましょう。
    • そもそも配列のポインタに限らず、生ポインタを意識しなくて済むコードを書くことで、メモリリークやオーバーフローアクセスの心配がなくなります。
      • vector, array, optional, shared_ptrなど今では生ポインタやdeleteを全く意識しなくて済むようになりました。(後述)
  • とはいえ生配列ポインタを直接返してくるライブラリが依然存在することも事実です。
    • libavcodecは様々な配列が生ポインタで扱われます。
    • libpython3ではPythonとC++での配列やり取りは生ポインタです。
    • OpenCVでもcv::Matの各要素を生配列ポインタで処理することがあリます。
  • 生配列のポインタが渡された場合、spanで受けてあげればその後のアクセスが安心になります。
    • 次の例ではライブラリのcallback時に、サイズがqos_countgranted_qosという配列を返してきます。
    • こんな場合はすかさずspanを利用しましょう。
      // Good!
      void on_subscribe(int mid, int qos_count, const int* granted_qos) {
      	span<const int> QoSs(granted_qos, qos_count); // ここ、span<>の使い所!!
      	for (const auto& q: QoSs) { // 万が一にもオーバーフローしない安全設計。
              cout << q << endl;
      	}
      	cout << q.size() << endl; // qos_countと同じだけど、同一インスタンスから取れるので安心
      }
      
      // Bad!
      void on_subscribe(int mid, int qos_count, const int* granted_qos) {
      	for (int i=0; i<qos_count; ++i) {
              cout << granted_qos[i] << endl; // iの安全性を気にしないといけない
      	}
      	cout << qos_count << endl; // qos_countが書き換えられていないか、気にしないといけない
      }
      

以上です。

補足1 コンパイル時の警告

  • clang-tidyというツールを使うと上記のような生配列アクセスがすべて警告され、arrayspanを使うように促されます。
  • 開発者全員がclang-tidyを使い、下記のようなコードが駆逐されることを祈るばかりです。
#include <span>
int main()
{
	constexpr size_t size{ 10 };
	int* c_array = new int[size]; // warning: [cppcoreguidelines-owning-memory]
	{
		int* ptr = c_array;
		while (ptr != c_array + size)
			*ptr++ = 3; // warning: [cppcoreguidelines-pro-bounds-pointer-arithmetic]
		c_array[2] = 5; // warning: [cppcoreguidelines-pro-bounds-pointer-arithmetic]
	}
	{
		std::span<int> spn_array{ c_array, size };
		for (auto& ref : spn_array)
			ref = 3;      // Good!
		spn_array[2] = 5; // Good!
	}
	delete[] c_array;
	return 0;
}

補足2 生ポインタを意識しなくて済むコードを書く

  • そもそも生ポインタを意識しなければならないコードは旧石器時代のコードです。自分で書くときは生ポインタを意識しなくて良いコードを書きましょう。
  • 次の例では、クラスMyClassの配列を、newして返すだけのメソッドcreate()を2種類用意しました。
    • 1つ目は、vectorを返すようになっているので、生ポインタやdeleteを気にすることなく処理を続けられます。
    • 2つ目は、古典的なC配列を返すようになっていてリスクがあります。
  • C++ではあらゆるケースにおいて、この例の1つ目のように生ポインタを意識しなくて済む設計ができるようになっています。
    • もし使いたいライブラリが次の例の2つ目のcreate()のような設計になってしまっていたらspanを使うことで安心して要素アクセスすることができます。
    • 所有権つきで配列を渡された場合はspanを使ったとしてもdelete[]を忘れないように注意が必要です。
#include <array>
#include <iostream>
#include <optional>
#include <span>
#include <vector>

using namespace std;

struct MyClass {
	MyClass() { cout << "new!: " << memberId_ << endl; }
	~MyClass() { cout << "delete!: " << memberId_  << endl; }
	void print() const { cout << memberId_ << endl; }

private:
	static uint32_t nextId_;
	uint32_t memberId_{ id_++ };
};

uint32_t MyClass::nextId_{ 0 };

// Good!
auto createVec(size_t size) -> vector<MyClass>
{
	vector<MyClass> ret(size);
	return ret; // このあとdeleteを気にしなくてもいい。
				// (moveなので、領域再割り当てやコピーのオーバーヘッドなし)
}

// Bad!!
auto createCArray(size_t size) -> tuple<MyClass*, size_t>
{
	return { new MyClass[size], size }; // 危険。どこかでdeleteが必要
}

int main()
{
	// Good!
	{
		auto v = createVec(2); // deleteを気にしなくてもいい。
		for (const auto& c : v)
			c.print();
	}
	
	// Bad!!
	{
		auto [v, size] = createCArray(3); // new MyClass[size]
		for (size_t i = 0; i < size; ++i) // iはいつでも安全?
			v[i].print();
		delete[] v; // delete。これを忘れると大変
	}
	
	// Bad but a little better
	{
		auto [v, size] = createCArray(2); // new MyClass[size]
		span<MyClass> V {v, size};
		for (const auto& c : V)
			c.print();
		delete[] v; // delete。これを忘れると大変
	}
	return 0;
}

実行するとこんな感じに出力されます。

new!: 0
new!: 1
0
1
delete!: 1
delete!: 0
new!: 2
new!: 3
new!: 4
2
3
4
delete!: 4
delete!: 3
delete!: 2
new!: 5
new!: 6
5
6
delete!: 6
delete!: 5
3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2