[C++]関数の引数として受け取る固定長配列の長さは指定可能である。

先の記事で配列型の扱いをいろいろ考えていた中で、ふと気がついた。一般に配列を受け取る関数は、配列引数を先頭要素へのポインタの形で受け取らざるを得ず、その配列の要素数を指定できないと思われている。確かに今や枯れ果てたC言語ではそうだった。が、実はC++であれば可能である。

//これはC言語仕様によりvoid print(char* a)と同じ。要素数は10でなくても動作してしまう。
void print(char a[10]) { printf(a); }
//これは"配列への参照"という特殊な型を要求しているため、要素数は10でなければならない。
void print(const char(&a)[10]) { printf(a); }

配列そのものではなく、配列への参照というC++特有の引数指定を行うと、どうやら要素数まできっちり判定してくれるようである。
私は単純な配列型が嫌いなのでこういうのは大抵std::arrayを使っており特に困ったことはないが、コードの文字数が多くなって鬱陶しいのも確かなので、人によっては役に立つテクニックかもしれない。

[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が配列非対応であることでコンパイルエラーになる話である。

Tensorflowによる連続値推定。

Tensorflowの最終的な出力が、分類ではなく何らかの連続値であってほしい場合がある。世間ではDeep Learning = 画像分類みたいなイメージが定着しているためか、サンプルを探してもそのようなものばかりであるが、連続値推定も可能である。例えば素粒子実験において、ある粒子の情報を入力してそのエネルギーを推定させたりできる。

方法は難しくない。通常、最終的な出力はsigmoid関数やsoftmax関数などの活性化関数を通し、交差エントロピー関数を最小化するようにフィッティングさせるが、活性化関数は結果を0か1に選り分けてしまうのでよろしくない。したがって、活性化関数を通さなければよい。そして交差エントロピー関数ではなくよくある最小二乗法のように、真の値に対する誤差のRMSを最小化させれば良い。

極めてシンプルな例を書くと、次のような感じだ。

x = tf.placeholder(tf.float32, shape = (None, n_in))
t = tf.placeholder(tf.float32, shape = (None, n_out))

w = tf.Variable(tf.random.truncated_normal([n_in, n_hidden]))
b = tf.Variable(tf.random.truncated_normal([n_hidden]))
h = tf.nn.sigmoid(tf.matmul(x, w) + b)

v = tf.Variable(tf.random.truncated_normal([n_hidden, 1]))
c = tf.Variable(tf.random.truncated_normal([1]))
y = tf.matmul(h, v) + c

loss = tf.reduce_mean(tf.square(t - y))
train_step = tf.train.GradientDescentOptimizer(0.1).minimize(loss)

真の値tに対して、推定値yのずれの二乗値を最小化させているだけである。もちろんRMSが望ましくない場合もあるだろうから、ユーザーが望むようにlossの式の形を変えること。