Array の operator[] が参照を返さない問題
juce::Array は、自身が保持している要素にアクセスするために operator[] を提供していますが、この API は保持している要素を値として返すようになっていて、 std::vector のように参照では返さない仕組みになっています。
このため、 operator[] で Array の要素を直接書き換えたり、要素への参照を得たりすることはできません。
juce::Array<int> arr;
arr.add(100);
// 出力: 100
std::cout << arr[0] << std::endl;
// コンパイルエラー!
arr[0] = 200;
// コンパイルエラー!
int &val = arr[0];
これは、参照を返すようにした場合、あとでその参照が無効になってしまった状態で参照を利用しようとするとプログラムがクラッシュするため、それを避ける目的でこのようになっているそうです。1
このように設計されているのは理解できますが、operator[] の仕様が C++ の配列や std::vector と異なるのは実際には分かりにくいと感じます。
特に、 Array が保持する要素の型が構造体やクラスになっている場合、上記のサンプルで起きていたような operator[] から取得したオブジェクトへの代入がコンパイルエラーにならない問題があります。
using ComplexType = std::complex<float>;
juce::Array<ComplexType> arr;
arr.add(ComplexType{ 0.1f, 0.2f });
// 出力: 0.05
std::cout << std::norm(arr[0]) << std::endl;
// コンパイルが通ってしまう!
arr[0] = ComplexType{ 0.3f, 0.4f };
// 上の代入処理では arr[0] の呼び出しから戻るときに
// 一時的に作られた一時オブジェクトに対して代入処理が行われる。
// そのため、実際は Array の要素は変更されない!
// 出力: 0.05
// 0.25を期待しているのにそのようになっていない!
std::cout << std::norm(arr[0]) << std::endl;
この問題に対して、いくつかの対処法が考えられます。
対処法
operator[] の代わりに getReference() メンバ関数を使用するようにする
juce::Array の getReference() メンバ関数を使用すると、指定した index 位置の要素の参照を取得できます。したがって std::vetor の operator[] と同じように使用できます。
JUCE が用意している API を順当に使用する方法なので、この方法が一番妥当かもしれません。
この方法の欠点は、単に[]で要素を参照するのに比べてコードが少し冗長になる点です。
またgetReference() メンバ関数は C++ 標準には無い関数なので、 operator[] を使った既存の C++ コードがあったときに、それを juce::Array を使うように移植しようとしたときに少し移植の手間が掛かります。
using ComplexType = std::complex<float>;
juce::Array<ComplexType> arr;
arr.add(ComplexType{ 1.0f, 2.0f });
// 出力: 0.05
std::cout << std::norm(arr[0]) << std::endl;
// operator[] の代わりに getReference() メンバ関数を使用する。
arr.getReference(0) = ComplexType{ 0.3f, 0.4f };
// 出力: 0.25(期待通り)
std::cout << std::norm(arr[0]) << std::endl;
data() メンバ関数で要素列を指すポインタを取得し、それに対して operator[] を使用する。
juce::Array クラスの data() メンバ関数を呼び出すと、要素列を指すポインタを取得できます。このポインタに対して operator[] を使用すれば、通常の C++ の配列のように各要素への参照を取得できます。
using ComplexType = std::complex<float>;
juce::Array<ComplexType> arr;
arr.add(ComplexType{ 1.0f, 2.0f });
// 出力: 0.05
std::cout << std::norm(arr[0]) << std::endl;
// data() メンバ関数を呼び出し、そのポインタに対して operator[] を呼びだす
arr.data()[0] = ComplexType{ 0.3f, 0.4f };
// 出力: 0.25(期待通り)
std::cout << std::norm(arr[0]) << std::endl;
この方法の欠点は、 data() メンバ関数を呼び出すのを忘れると、コード2の例と同じ状態になるため、 Array の要素を変更したはずができていないというバグを生みやすい点です。2
ArrayBase クラスを利用する
juce::ArrayBase クラスは、 juce::Array や他のコンテナクラスで内部的に使用されているクラスです。
juce::Array クラスは operator[] が参照ではなく値を返すデザインになっていますが、 juce::ArrayBase クラスは operator[] で参照を返すデザインになっています。
そのため、 Array の代わりに ArrayBase クラスを使用することで、期待通り operator[] を使用できます。
ただし、 ArrayBase クラスは内部的に使用されているクラスなので juce::Array が用意している様々な API は ArrayBase を使う場合は利用できなくなります。
// juce::Array ではなく juce::ArrayBase クラスを利用する
juce::ArrayBase<ComplexType, juce::DummyCriticalSection> arr;
arr.add(ComplexType{ 0.1f, 0.2f });
// 出力: 0.05
std::cout << std::norm(arr[0]) << std::endl;
arr[0] = ComplexType{ 0.3f, 0.4f };
// 出力: 0.25(期待通り)
std::cout << std::norm(arr[0]) << std::endl;
operator[] が参照を返すような Array クラスを用意して使用する。
以下のようにして、 juce::Array と同じ API を提供しつつ operator[] の定義だけが異なるようなクラスを定義できます。
template<class T, typename TypeOfCriticalSectionToUse = juce::DummyCriticalSection, int minimumAllocatedSize = 0>
class ReferenceableArray : private juce::Array<T, TypeOfCriticalSectionToUse, minimumAllocatedSize>
{
public:
using base_type = juce::Array<T, TypeOfCriticalSectionToUse, minimumAllocatedSize>;
template<class ...Args>
ReferenceableArray(Args&& ...args)
: base_type(std::forward<Args>(args)...)
{}
base_type & base() { return static_cast<base_type &>(*this); }
base_type const & base() const { return static_cast<base_type const &>(*this); }
ReferenceableArray(ReferenceableArray const &rhs)
{
base() = rhs.base();
}
ReferenceableArray & operator=(ReferenceableArray const &rhs)
{
base() = rhs.base();
return *this;
}
ReferenceableArray(ReferenceableArray &&rhs)
{
base() = std::move(rhs.base());
}
ReferenceableArray & operator=(ReferenceableArray &&rhs)
{
if(this == &rhs) { return *this; }
base() = std::move(rhs.base());
return *this;
}
using base_type::clear;
using base_type::clearQuick;
using base_type::fill;
using base_type::size;
using base_type::isEmpty;
using base_type::getUnchecked;
using base_type::getReference;
using base_type::getFirst;
using base_type::getLast;
using base_type::getRawDataPointer;
using base_type::begin;
using base_type::end;
using base_type::data;
using base_type::indexOf;
using base_type::contains;
using base_type::add;
using base_type::insert;
using base_type::insertMultiple;
using base_type::insertArray;
using base_type::addIfNotAlreadyThere;
using base_type::set;
using base_type::setUnchecked;
using base_type::addArray;
using base_type::addNullTerminatedArray;
using base_type::swapWith;
using base_type::resize;
using base_type::addSorted;
using base_type::addUsingDefaultSort;
using base_type::remove;
using base_type::removeAndReturn;
using base_type::removeFirstMatchingValue;
using base_type::removeAllInstancesOf;
using base_type::removeIf;
using base_type::removeRange;
using base_type::removeLast;
using base_type::removeValuesIn;
using base_type::removeValuesNotIn;
using base_type::swap;
using base_type::move;
using base_type::minimiseStorageOverheads;
using base_type::ensureStorageAllocated;
using base_type::sort;
using base_type::getLock;
T & operator[](int index)
{
return base().getReference(index);
}
T const & operator[](int index) const
{
return base().getReference(index);
}
};
このクラスを使用すると以下のようにコードを書けるようになります。
ReferenceableArray<ComplexType> arr;
arr.add(ComplexType{ 0.1f, 0.2f });
// 出力: 0.05
std::cout << std::norm(arr[0]) << std::endl;
arr[0] = ComplexType{ 0.3f, 0.4f };
// 出力: 0.25(期待通り)
std::cout << std::norm(arr[0]) << std::endl;
ReferenceableArray クラスは juce::Array クラスを private 継承しています。これは意図せぬ型変換によって期待しない挙動が起こるのを防ぐためです。
juce::Array を期待するコードに ReferenceableArray を渡すには base() メンバ関数を呼び出します。
void foo(juce::Array<ComplexType> const &arr)
{
for(auto &elem: arr) { /* ... */ }
}
ReferenceableArray arr<ComplexType> arr;
arr.add(ComplexType{ 0.1f, 0.2f });
// コンパイルエラー!
foo(arr);
// base() 関数を呼び出すと、 自身を指す juce::Array 型の参照を得られる。
// juce::Array を期待する関数にはこの参照を渡す。
foo(arr.base());
この方法の欠点は、 juce::Array とは別の型を使用することになるので、例えば juce::Array を返すような関数から返ってきた Array をそのまま ReferenceableArray として利用できないことです。(関数から返された juce::Array の各要素を一旦 ReferenceableArray にコピーしてから使うことになる)
これを考慮すると、もしかすると ReferenceableArray のような自身が要素を保持するようなクラスを使用するのではなく、 Array の参照を保持するようなラッパークラスを用意し、ラッパークラスの operator[] で Array の要素への参照を返すという方法のほうが適用範囲が広くていいかもしれません。
-
https://forum.juce.com/t/arrays-why-is-the-index-operator-only-one-way/3826/4?u=hotwatermorning ↩
-
筆者は実際に data() メンバ関数の呼び出しを忘れたことでバグが発生して数時間そのバグと格闘する羽目になりました。 ↩