配列への参照型について
C++で配列への参照型変数を定義するには次のように書きます。
int arr[4] = {0, 1, 2, 3};
int (&a)[4] = arr;
&と変数名を一緒に囲ってやれば良いです。ちなみに、配列への参照型を単体で書くとこうなります。
using array_t = int(&)[4];
では、配列への参照を返す関数はどのように書けば良いでしょうか。答えはこうです。
int arr[4] = {0, 1, 2, 3};
int (&func())[4] {
return arr;
}
なぜこうなるのかというと、まず普通のintなどを返す関数について考えてみましょう。
宣言はこうですね。
int func();
さて、これをint型の変数宣言と見比べてみましょう。
int a;
int func();
この2つを見比べていると、変数aの宣言は「aの型がintである」と読めるので同様に、関数funcの宣言も「(funcに括弧をくっつけたもの)の型がintである」というふうに読めてこないでしょうか。この見方で配列参照へと戻ってみます。
int (&arr)[4];
int (&func())[4];
ということで、変数arrの宣言が「arrの型がint(&)[4]である」ということと同じように、関数funcの宣言も「(funcに括弧をくっつけたもの)の型がint(&)[4]となる」というふうに見てみると、この一見奇妙な定義方法も納得できるのではないでしょうか。
配列参照への型変換演算子
では配列の参照型への型変換演算子はどのように書けばよいでしょうか。普通の、例えばintへの型変換演算子は次のように書きますね。(以後のコードは構造体定義の中で書かれてるものとして適宜見てください)
operator int() {
return 42;
}
上記での説明から配列参照の場合を類推すると、こんな感じでしょうか。
int arr[4] = {0, 1, 2, 3};
operator int (&())[4] {
return arr;
}
しかし、これは文法エラーでコンパイルエラーとなります。ではこうでしょうか。
int arr[4] = {0, 1, 2, 3};
operator int(&)[4]() {
return arr;
}
残念ながらこれもコンパイルエラーです。ではどのように書くのが正解なのでしょうか。次のように書いてみるとどうでしょう。
int arr[4] = {0, 1, 2, 3};
(&operator int())[4] {
return arr;
}
実はこのコードは「gccでは」コンパイルが通ります。他のclangなどのコンパイラではこのコードは通りません。ではこのコードはgccのときだけ、拡張やらなんやらの力によって正常に動いているということなのかというと、実はそういうことでもありません。更にここへintへの型変換演算子も追加してコンパイルしてみます。
#include <iostream>
struct A {
int arr[4] = {0, 1, 2, 3};
(&operator int())[4] {
return arr;
}
operator int() {
return 42;
}
};
int main() {
A a{};
for (int i : static_cast<int(&)[4]>(a)) {
std::cout << i << std::endl;
}
std::cout << static_cast<int>(a) << std::endl;
}
すると、普段とは少し違うタイプのコンパイルエラーが発生します。
$ g++ hoge.cpp
/tmp/ccL3WxXd.s: Assembler messages:
/tmp/ccL3WxXd.s: 30: Error: symbol `_ZN1AcviEv' is already defined
何が起こっているのでしょうか。配列参照への型変換演算子だけが定義された状態の、コンパイルが通ったあとのバイナリからシンボル情報を確認してみるとどういうことなのかがわかります。
$ nm a.out --demangle | grep "A::"
0000000000001240 W A::operator int()
なんと、配列参照への型変換演算子を定義したはずなのにシンボル名がintへの型変換演算子のものとなっています。追加でintへの型変換演算子を追加するとコンパイルエラーが発生していたのは、シンボル名が重複していたのが原因ということですね。
いずれにせよ上記のような定義の仕方は正しくなく、実際はこのように書いてはいけないということになります。
(gccでこのコードのコンパイルが通ってしまうのはなんでなんでしょうね、バグ?規格的にはどうなの?)
配列参照への型変換演算子の正しい書き方
ではどう書くのが正解なのかというと、次のように型エイリアスを使うのが正解です。
int arr[4] = {0, 1, 2, 3};
using array_type = int[4];
operator array_type&() {
return arr;
}
あるいは、標準ライブラリのtype_identityなどのメタ関数を一旦経由するという方法もあります。ただしtype_identityはC++20からです、それ以前を使用している場合は同等のものが簡単に作れるので自作しても良いですね。
int arr[4] = {0, 1, 2, 3};
operator std::type_identity_t<int(&)[4]>() {
return arr;
}
// あるいはtype_identityを自作する(C++17まで)
template<typename T>
struct type_identity { using type = T; };
template<typename T>
using type_identity_t = typename type_identity<T>::type;
operator type_identity_t<int(&)[4]>() {
return arr;
}
こうしてコンパイルすると、シンボル情報も正しく配列参照への型変換演算子を表すようになります。
$ nm a.out --demangle | grep "A::"
0000000000001240 W A::operator int (&) [4]()
どうやら配列参照への型変換演算子を定義するには直接型名を書くことはできず、必ず型エイリアスやメタ関数を経由しなくてはいけないことになっているようです。若干残念ですね...。
参考
https://onihusube.hatenablog.com/entry/2018/12/14/013900
https://stackoverflow.com/questions/20308716/how-to-write-a-c-conversion-operator-returning-reference-to-array
https://stackoverflow.com/questions/2615281/what-is-the-iso-c-way-to-directly-define-a-conversion-function-to-reference-to