はじめに
- ライブラリから配列が渡された場合、オーバーフローさせない自信はありますか?
標準ライブラリ <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_count
のgranted_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が書き換えられていないか、気にしないといけない }
- 次の例ではライブラリのcallback時に、サイズが
以上です。
補足1 コンパイル時の警告
-
clang-tidyというツールを使うと上記のような生配列アクセスがすべて警告され、
array
やspan
を使うように促されます。 - 開発者全員が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配列を返すようになっていてリスクがあります。
- 1つ目は、
- C++ではあらゆるケースにおいて、この例の1つ目のように生ポインタを意識しなくて済む設計ができるようになっています。
- もし使いたいライブラリが次の例の2つ目のcreate()のような設計になってしまっていたら
span
を使うことで安心して要素アクセスすることができます。 - 所有権つきで配列を渡された場合は
span
を使ったとしてもdelete[]
を忘れないように注意が必要です。
- もし使いたいライブラリが次の例の2つ目のcreate()のような設計になってしまっていたら
#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