配列とサイズの切っても切れない関係
最近出逢った問題について話そうと思います。
以下のクラスのメンバー変数の生配列をゲッターで返したい場合はどうしますか?
class Test
{
public:
Test() {};
// ここに m_data のゲッターを追加したい
private:
static constexpr std::size_t DATA_COUNT{10};
int m_data[DATA_COUNT]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
};
もちろんもっと複雑な構造になっていましたが、色々省略しました。
DATA_COUNTをstatic constexprにしたのは、生配列の長さはコンパイル時に決まっていないといけないのと、単純にマクロを使いたくなかったからです。
m_dataが生配列ではなく、std::arrayやstd::vectorだったら、そのまま参照かポインタで返せばいいだけの話ですが、どうしても生配列でないといけない場合は、そう簡単にいかないです。
初歩的な問題ではありますが、C++20 の時点で一番いい方法は何なのか?を考えてみました。
出力引数
一番最初には出力引数を使った方法で、絶対お勧めしない例として紹介します。
int getData(int* buf, std::size_t size) const
{
if (size < DATA_COUNT * sizeof(int))
{
return -1;
}
memcpy(buf, m_data, size);
return 0;
};
ただのゲッターなのに、バッファとそのサイズを渡さないといけなくて、中で生配列をコピーまでして、エラーになるケースも存在するので、エラーコードも返します。
以下のような非常に不便な使い方になります。
constexpr std::size_t DATA_COUNT{10};
auto data = std::make_unique<int[]>(DATA_COUNT);
int ret = test.getData(data.get(), DATA_COUNT * sizeof(data));
if (ret < 0)
{
std::cout << "error" << std::endl;
}
バッファーは呼び出し側で用意しないといけないため、サイズも別で返してあげないと、呼び出し元でもサイズを指定しないといけなくなります。
ゲッターはできるだけ簡単であるべきなので、お勧めできません。
戻り値として直接返す
やはりこれが一番先に思い浮かびます。
const int* getData() const
{
return m_data;
};
コピーはなくて、エラーケースもないです。ようやくゲッターらしくなってきました。
でも問題は、生配列は直接返すと、ただのポインタに成り下がってしまうことです。返されるint*は配列へのポインタなのか、普通のint型の変数へのポインタなのかは分からなくなります。
生配列の要素数は以下のように計算します。
int data[10]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
std::cout << sizeof(data)/sizeof(data[0]) << std::endl; // 40 / 4 = 10🤗
因みにですが、C++17 からstd::size()という上記の計算をしてくれる便利な関数が追加されました。以下のように使えます。
int data[10]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
std::cout << std::size(data) << std::endl; // 10
ポインタの場合は、以下の結果になります。
int* pData = data;
std::cout << sizeof(pData)/sizeof(pData[0]) << std::endl; // 8 / 4 = 2☠️
// ※sizeof(pData)は実行環境によって別の値になる可能性はあります
当たり前ですが、sizeof(pData)はポインタのサイズになってしまいます。
では、ポインタ先の方を使ってみましょう。
std::cout << sizeof(*pData)/sizeof(pData[0]) << std::endl; // 4 / 4 = 1☠️☠️
ポインタ先に生配列の最初の要素のint型の変数があるだけなので、生配列が一回ポインタに成り下がってしまったら、要素数を計算できなくなります。
そのため、要素数を別で返さないといけなくなります。
以下のように要素数返す別の関数を追加してもいいのですが、
std::size_t getDataSize() const
{
return DATA_COUNT;
};
ただのゲッターのために2つの関数も用意するのは API として微妙ですよね。
もちろん以下のように専用の構造体を使って、一緒にセットで返せますが、
struct Data
{
int* ptr;
std::size_t count;
};
Data getDataSize() const
{
return {m_data, DATA_COUNT};
};
でも毎回こういう構造体を用意するのも面倒です。
ただ C++20 からはstd::spanというユーティリティクラスが追加されましたので、それが使えますね。
#include <span>
std::span<const int> getData() const
{
return m_data;
};
std::span自体は以前こちらでも紹介したことがありました。
C++20 を使える環境だったら、これが個人的に一番いい方法だと思います。
参照で返す
ポインタで返したら、要素数が分からなくなるため、参照で返せばいいのではないですか?
普通の型だったら、そうすると思います。でも生配列の参照って見かけないですよね。
C 言語で生配列の参照という概念はなかったせいなのか、実は C++ で参照できるようになりましたが、あんまり知られていないです。
以下のように書きます。
const int (& getData() const)[10]
{
return m_data;
};
何これ?!って感じですよね。これだったら、呼び元でサイズの計算もできるのですが、殆どの人は見たこともないため、こういうのを使ったら、みんな困惑すると思います。
この関数を呼ぶ時も、autoを使わない限り、殆どの人は戻り値の型もどうやって書けばいいのか分からなくなると思います。
const int(&data)[10] = test.getData();
それに、この書き方だと、要素数が関数のシグネチャに含まれていて、要素数を変えるたびに、ゲッターとゲッターを使うコードを全部一緒に変えないといけなくなるので、メンテが大変です。
お勧めはできないですが、C++ では一応生配列は参照で返せることを紹介したかっただけです。
構造体でラッピング
生配列を構造体でラッピングすると、参照型でもポインタでも返せるようになります。
struct Data
{
int data[DATA_COUNT];
};
const Data& getData() const
{
return m_data;
};
const Data* getData() const
{
return &m_data;
};
こうすると、構造体の参照やポインタになるため、普通の型のような挙動になります。サイズも計算できるようになります。
std::cout << std::size(test.getData().data) << std::endl;
後、生配列をゲッターの中でstd::arrayかstd::vectorに変換してから、返すこともできますが、どうしてもコピーが必要になってしまうため、紹介しませんでした。
これ以外もい色々方法はあると思いますが、どの方法が一番いいと思いますか?
あなただったら、どうやって返しますか?
Discussion