[C++]テンプレートによる配列の扱い。std::anyは配列を格納できるのか。

テンプレートは配列型をどのように推定するのか。

C++のテンプレートを使っていて稀に遭遇する問題の一つは、配列の取り扱いである。私はC言語時代の配列を毛嫌いしていて基本的にstd::arrayを使うようにしているのでこの問題には滅多に引っかからないのだが、汎用ライブラリを設計しようと思うとそうも言っていられない時がある。

通常、テンプレートの型推論では配列型はポインタへと変換される。例えば、

template <class Type>
void print(Type a) { std::cout << a << std::endl; }

という関数があったとして、

print("abcde");

と呼び出せば、print関数のTypeはconst char*型と推定される。"abcde"は本質的にはchar[6]型であるが、これをテンプレートが勝手に変換しているのだ。 ただし困ったことも起こる。print関数が次のような形だったらどうだろう?

template <class Type>
void print(const Type& a) { std::cout << a << std::endl; }

引数をconst referenceにするのは関数テンプレートではよくあることだろう。このとき、上と同じように引数に"abcde"を与えてみると、果たしてTypeは何と推定されるだろうか。
答えは、char[6]型である。char*ではない。つまり、void print(const char(&a)[6])という関数を呼び出していると扱われるのである。

Poco::Anyやstd::anyと配列型。

今回これがちょっと問題を起こした。Poco::formatに欠陥があったのだ。研究室の同期が問題を報告してきた。彼の書いたソースコード自体は見ていないが、多分次のようなことをしようとしたのだと思う。

enum A { ALPHA, BETA, };
A a = ALPHA;
if (a == ALPHA) std::cout << Poco::format("a == %s\n", "ALPHA");
else std::cout << Poco::format("a == %s\n", "Beta");

上のコード、一見なんの問題もなさそうであるが、実はコンパイルできない。

Poco::formatはテンプレートを使わない超力技実装である。私ならこんな糞実装は絶対に御免だが、意地でも.libや.dllに押し込みたかったのかもしれない。

std::string format(const std::string& fmt, const Any& arg1);

重要なのは、引数がPoco::Anyに押し込まれること、そしてPoco::Anyが配列に対応していないことである1。 Anyのコンストラクタは次のようになっている。

template<typename ValueType>
Any(const ValueType & value)
{
    construct(value);
}

値をconst referenceで受け取り、std::decayもしていないのだ。これはconstruct関数中でも同様で、だからconst char(&)[N]型と判定された引数をそのまま代入しようとし、配列から配列への代入ができないことでコンパイルエラーとなっていた。広く公開されているライブラリにしては頭の悪い実装だが、はっきり言ってこのあたりはC言語仕様の欠陥でもあるので、Pocoを責め立てることも難しい。私だってAnyを配列に対応させようなんて思わない。ただ、これをformatに使ってしまったのは阿呆としか言いようがない。多分、だからこそ、"%s"がstd::stringにしか対応していないのだろうが。

std::anyであれば配列も一応代入可能であるが、実際に代入されるのは先頭要素へのポインタとなる。配列の要素自体がコピー代入されるわけではないので、元となった配列それ自体の寿命には気を配らなければならない。

char c[] = "abcde";
std::any a(c);
std::cout << std::any_cast<char*>(a);
//これは動作するが、cの消滅とともにaの中身も消滅する。
//間違っても
//std::any a("abcde");
//なんてしないように。即座に"abcde"の寿命が尽きてaの中身が保証されなくなる。はず。

配列をコピー代入できるAny。

では配列そのものを格納できるようなAnyクラスは作れないのか?いや、作れる。即席で超いい加減だが重要なところだけ実装してみた。

class Any
{
    struct PlaceHolder
    {
        virtual ~PlaceHolder() {}
    };
    template <class Type>
    struct Holder : PlaceHolder
    {
        Holder(const Type& v) : mValue(v) {}
        Type mValue;
    };
    template <class Type, size_t N>
    struct Holder<Type[N]> : PlaceHolder
    {
        Holder(const Type(&v)[N])
        {
            for (size_t i = 0; i < N; ++i) mValue[i] = v[i];
        }
        Type mValue[N];
    };

public:

    template <class Type>
    Any(const Type& v) : mHolder(std::make_unique<Holder<Type>>(v)) {}

    template <class Type>
    const Type& Get() const { return static_cast<const Holder<Type>*>(mHolder.get())->mValue; }

private:
    std::unique_ptr<PlaceHolder> mHolder;
};

//次のように要素を取得できる。
int main()
{
    Any a("abcde");//配列"abcde"はaの中にコピーされるので、寿命の心配はない。
    std::cout << a.Get<char[6]>() << std::endl;
    return 0;
}

このように、配列を受け取った場合だけ値を格納する本体(Holder)を部分特殊化すれば良い。

しかし、Pocoの設計の愚かしさ以上に、C言語流配列の愚かしさを痛感せずにいられない。ライブラリ設計者以外のC++erはあんなもんバッサリ切り捨てるべきである。マクロと一緒に絶滅しろ。


  1. 実はもっと酷いことに、これに加えてPoco::formatはconst char*をそもそも受け取れないという二重の罠が仕掛けられているのだが、それはまた別の話。今回はAnyが配列非対応であることでコンパイルエラーになる話である。