ボレロ村上 - ENiyGmaA Code

中3女子です。

constexpr本を執筆することにした

中3女子です。


constexpr本を執筆しようと思い立った。
constexpr について、言語機能、用途、歴史的経緯、そして実装技術の観点から詳細に解説した本である。

動機

C++本のうち、例えば template や template ライブラリについて解説した本は探せばいくらでもある。
「Moderrn C++ Design」のような古典的名著から、「C++テンプレートテクニック」のような日本人の著作まで、バイブルと呼ぶべき本が多く存在する。
偉大なプログラマ達が《発見》し発展させたテンプレートメタプログラミングといった技法を、現代のわれわれは当たり前のように学ぶことができる。
解説サイトも沢山ある。ネット上には一線級の C++ プログラマの書いた技術情報とコードが溢れている。
たとえ英語が全く使えない者でも、ネット環境と最低限の検索能力さえあれば、template に関する日本語資料に困ることはない。


では、constexpr についてはどうだろうか?
以前、自分は constexpr を使うべき5の理由 - まとめ&リンク集 - ボレロ村上 - ENiyGmaA Code というエントリを書いた。
constexpr に関する纏まった日本語情報は多くない。
検索すれば、constexpr による Zero-initialization の議論など、興味深いいくつかの情報を見つけることができるが、断片的だ。
日本語圏以外であたればここに載っている以外の様々なアイディアや知見を見つけることができるだろうが、やはり総合的なものは聞かない。
書籍では、江添氏が「プログラミングの魔導書 Vol.2」で書いた 10 ページほどの constexpr 入門がある。
最近書かれた C++ 本には、もしかすると constexpr について言及された節もあるかもしれないが、確認していない。
ともかく、constexpr に関して、総合的かつ専門的に詳細にわたって解説された書籍またはサイトというものは、未だ見聞きしたことはない。
(もしあるならば、ぜひ知らせて戴きたい)


無いのであれば、誰かが書くしかない。
しかるに、constexpr という言語機能は奥の深いものではあるが、ビジネスプログラマが取り上げるには実務的でないし、言語研究者が取り上げるには実際的すぎる。
ゆえに、自分が書くしかないと思い至った次第である。
幸いにして自分は、constexpr に関する実装経験については、少なくとも日本において一線級にあると自負している。
筆無精ゆえ満足な数ではないが、いままでブログエントリや勉強会の発表に際して、いくつかの constexpr に関する技術情報を書いてきた。
そして Sprout ライブラリの実装にあたって、これまで文章にしてきたことより遙かに多くの知見を得ている。
それらを纏めて詳細な解説として本にしようと考えている。
すべての C++ プログラマに資するものにはならなくとも、少なくとも世界に一冊はそのような本が必要なはずである。

内容

さて、内容的にはおおまかに以下のように考えている。

    1. 定数式の歴史的経緯
      • まずは、C言語の define マクロによる定数から始まって、コンパイル時定数と実行時定数、テンプレートメタによる定数や enum ハック、C++11 策定にあたっての定数式の定義の議論、そして constexpr に至るまでの経緯を、歴史的に見ていく。
      • これによって、constexpr がなぜ必要だったのかを明確にする。
    2. constexpr の言語機能の基本的な紹介
      • 「中3女子でもわかる constexpr」の前半や江添氏の「constexpr 入門」で取り上げられたような本当に基礎的な constexpr の動作、また「constexpr を使うべき5の理由」で取り上げたような基本的な用途、および規格の内容を噛み砕いて纏める。
      • これをしっかり纏めることで、constexpr を使ってみようと思ったプログラマが(これで本当に正しいのか?)と悩むことは無くなるはずである。
    3. constexpr の実装技術
      • Sprout ライブラリでは、constexpr でのプログラミングにおける、規格上あるいはコンパイラの制約からくるあらゆるイディオムを随所で用いている。
      • そのため、Sprout ライブラリの設計と実装をモチーフとして、その実装技術を解説していこうと考えている。
      • できれば、汎用アルゴリズムからレイトレーシングなど固有分野まで全般的にカバーしたい。
    4. constexpr がこの先生きのこるには
      • constexpr が有用であるのは間違いないが、不足や歪さも多く存在する。
      • 類似の機能である D言語の CTFE や、関数型言語Haskell 等)との比較をもとに、constexpr の現状の問題点を明らかにする。
      • その上で、この先 constexpr に求められる機能や思想について考える。


構成としては、かの Modern C++ Design を参考にしようと思う。
というのも、MC++D は Loki というライブラリをモチーフとして、template ライブラリのデザイン論を展開する構成である。
本書では Sprout ライブラリをモチーフとして構成すると考えれば、大いに真似しやすいと思われるからである。


執筆期間については、これから内容を詰める課程で決めていこうと考えている。
まずはおおよその分量(ページ数)の目安を決めなければならない。
例えば「プログラミングの魔導書」は A4 弱のサイズで 200 ページ程で一つの記事は 10〜30 ページ、「C++テンプレートテクニック」はおよそ A5 サイズで 300 と数十ページ程である。
自分としては、とりあえず今年中には公開までいきたいと思っている。


公開の形態は、AmazonKindleストアで電子書籍を個人で販売できるので、それを利用しようかと思っている。
個人的にデッドメディアである紙の本にも執着があるので、同じく Amazon のプリントオンデマンド(POD)も考えに入れている。
商業的な需要は見込めないので(それにまだ何も出来ていないわけだから)出版社への売り込みなどはまったく考えていないが、それはそれ。

査読のお願い

さて、執筆にあたってはいくつか関門がある。
先に述べたように、自分は constexpr に関する実装経験については自負するものがある。
しかしながら、自分にはプログラマの業務経験はない。
情報科学の専門教育を受けたことも無ければ、学術論文等の執筆経験もない。
C++ の言語規格には多少心得があるが、自分の知る C++標準化委員らのそれに及ぶほど厳密性のある知識ではない。
ゆえに、言語規格の深い部分と、学術的に厳密な部分(例えば計算量のオーダー等)に関して、しかるべき人に査読やアドバイスを請わなければならない。


そのため、C++ に一家言を持つと自負する方で、constexpr本の執筆を応援して戴ける方の有志に、査読をこの場でお願いしたいと思う。
連絡は Twitter @bolero_MURAKAMI またはメール contact-lib@boleros.x0.com まで。
質問等は上記連絡先やコメント欄などどこでも受け付けます。
よろしくお願いいたします。

宣言に const がないということの利点について考える

※これはネタ記事です
※この記事の内容をコピペしたり参考にすることで、HDDが初期化されたり、鼻から悪魔が出たり、遠隔操作ウィルス事件の犯人として逮捕されたとしても、筆者は一切責任を負いません

C++ソースコードで、変数やメンバ関数の宣言に const がないということを否定的にとらえる人もいるかと思います。
特にローカル変数にもどんどん const を付けていこうというような信条を持った const教信者の人にとっては、const がないということが不安材料として目に映ることが多いのではないかと思います。


けれども、const がないということは、本当に素晴らしいことです。
const がないことによって、たくさんの面倒から解放されるからです。

どのような時点でも代入できる

まず基本的なこととして宣言に const がなければどのような時点でも代入できるということです。
つまり、定数であるかどうかを意識する必要がありません。

まずは、const がどれだけ邪魔な存在か見てみましょう。

#include <stdio.h>
using namespace std;

const char* const __MOJIRETSU__;    // error: uninitialized const ‘__MOJIRETSU__’
int main() {
    __MOJIRETSU__ = "f*ck";         // error: assignment of read-only variable ‘__MOJIRETSU__’
    __MOJIRETSU__[1] = 'u';         // error: assignment of read-only location ‘*(((const char*)__MOJIRETSU__) + 1u)’
    printf("%s\n", __MOJIRETSU__);
}

変数 __MOJIRETSU__ になんと2つも const がついています!
(最初の const がポインタの指す先に対する const で、後の const が変数 __MOJIRETSU__ そのものに対する const だそうです)
こんなややこしいことをするせいで、こんな短いプログラムなのにコンパイルエラーが3つも出てしまいました。

    1. const 変数は明示的に初期化しなければならないので、コンパイルエラー
    2. const 変数には再代入できないので、コンパイルエラー
    3. ポインタの指す先も const なので、コンパイルエラー
#include <stdio.h>
using namespace std;

char* __MOJIRETSU__;
int main() {
    __MOJIRETSU__ = "f*ck";
    __MOJIRETSU__[1] = 'u';
    printf("%s\n", __MOJIRETSU__);
}

このように const を消すだけでコンパイルが通ります。
実行はしていませんが、こんな短いプログラムなので、結果は見るまでもないでしょう。

記述量がとても短くなる

typedef const char* const* const* const* const* const* const* const* const* const* const* const* const* const* const* const* const __TYPE__;
typedef char**************** __TYPE__;

見比べてみれば一目瞭然です。

宣言 const がないと変更に強い

最初から const を書かなければ、後で定数を書き換えたくなったときも、わざわざキャストをしたり、元の宣言を書き換えたりする必要がありません。

#include <stdio.h>
using namespace std;

const char* const __MOJIRETSU__ = "";
int main() {
    printf("%s\n", __MOJIRETSU__);

    /* ここで書き換えたくなった。 */
    const_cast<char*&>(__MOJIRETSU__) = "f*ck"; // わざわざキャストしなければならない
    printf("%s\n", __MOJIRETSU__);
}

ちなみに、Cスタイルキャストはよく知らないけど使っては駄目らしいので、const_cast を使います。これだけで一気に C++ らしいコードになります。
もちろん、最初から const を書かなければ、こんな面倒をする必要はありません。

関数のオーバーロードが不要になる

#include <stdio.h>
using namespace std;

typedef struct __CLASS_TAG__ {
    int __VALUE__;
    int& __GET__() { return __VALUE__; }
    const int& __GET__() const { return __VALUE__; }
} __CLASS__;

__CLASS__ __HENSUU__;
int main() {
    printf("%d\n", __HENSUU__.__GET__());
}

このように、ゲッター関数には const をつける風習もあるようですが、そもそも const は必要ありません。

#include <stdio.h>
using namespace std;

typedef struct __CLASS_TAG__ {
    int __VALUE__;
    int& __GET__() { return __VALUE__; }
} __CLASS__;

__CLASS__ __HENSUU__;
int main() {
    printf("%d\n", __HENSUU__.__GET__());
}

見ての通り、constオーバーロードを消しても、まったく問題なくコンパイルできるし動作します。

宣言に const がないと変更して欲しくない変数がいつ変更されるかわからないという批判に答える

文脈を追って読むとどの変数が変更して欲しくないものか、たいていはすぐにわかります。
const がないソースコードを読むことに慣れてしまえば、何の問題もないと思います。

邪魔な const を排除するには

自分で書いたコードならともかく、サードパーティのライブラリには、困ったことに const を多用していたりするものがあります。
これを排除しましょう。


まずは先程も挙げたキャストです。

#include <stdio.h>
using namespace std;

typedef struct __CLASS_TAG__ {
    int __VALUE__;
    const int& __GET__() const { return __VALUE__; }
} __CLASS__;

__CLASS__ __HENSUU__;
int main() {
    reinterpret_cast<double&>(const_cast<int&>(__HENSUU__.__GET__())) = 3.14;
    printf("%d\n", __HENSUU__.__GET__());
}

const_cast や reinterpret_cast を活用することで、const を使ったクラスであっても自由自在に操作できます。

#include <stdio.h>
using namespace std;

#define const /* This is KOOL hack! */
typedef struct __CLASS_TAG__ {
    int __VALUE__;
    const int& __GET__() const { return __VALUE__; }
} __CLASS__;

__CLASS__ __HENSUU__;
int main() {
    __HENSUU__.__GET__() = 100;
    printf("%d\n", __HENSUU__.__GET__());
}

これは面白くて気の利いたハックです。
マクロで const を単なる空白に置換することで、無効化しているのです。

宣言に const がないことのデメリットはないのか

あげるとすれば、コンパイル時定数です。
配列変数を宣言するときの要素数を指定する場合などは、コンパイル時定数でなければなりません。
しかしこのような場合も、わざわざ定数を定義したりせず、マクロを使えばよいでしょう。


もうひとつは、いつどこで値が書き換えられるかまったく保証できないので危険ということですね。
これについては仕方ないので諦めてください。

constexpr アルゴリズムの実装イディオム その1


中3女子です。


今回は、constexpr におけるアルゴリズムの実装法について考える。


よく知られているように、constexpr 関数には言語規格上の制約が多くある。
ローカル変数が使えない、if や for などの制御構文が使えないなどは、C++11 に触れた者なら誰でも知っているだろう。
だが、それらは条件演算子再帰によって、単純に代替できる問題である。
つまり、非 constexpr な実装に対して、処理の流れを本質的に変えることなく constexpr に書き換えることができる。

    • 単純に書き換えられる例:
template<typename T>
T const& runtime_min(T const& a, T const& b) {
    if (b < a) return b;
    return a;
}

template<typename T>
constexpr T const& constexpr_min(T const& a, T const& b) {
    return b < a ? b
        : a
        ;
}


しかしながら、単純な書き換えでは解決できない問題もある。
例えばコンパイル時の再帰深度の制限などである。
そのため、constexpr でより良いアルゴリズムを実装するためには、ケースに応じたイディオムによって constexpr 特有の問題を解決する必要がある。

distance の実装

std::distance(first, last) は、[first, last) なるイテレータ間の距離を求める関数である。
典型的な実装は以下のようになる。

    • std::distance
template<typename RandomAccessIterator>
typename std::iterator_traits<RandomAccessIterator>::difference_type
distance_impl(RandomAccessIterator first, RandomAccessIterator last, std::random_access_iterator_tag*) {
    return last - first;
}

template<typename InputIterator>
typename std::iterator_traits<InputIterator>::difference_type
distance_impl(InputIterator first, InputIterator last, void*) {
    typename std::iterator_traits<InputIterator>::difference_type n = 0;
    for (; first != last; ++first) {
        ++n;
    }
    return n;
}

template<typename InputIterator>
typename std::iterator_traits<InputIterator>::difference_type
distance(InputIterator first, InputIterator last) {
    typedef typename std::iterator_traits<InputIterator>::iterator_category* category;
    return distance_impl(first, last, category());
}


これを“単純に” constexpr に書き直すと、以下のようになる。

#include <sprout/iterator/next.hpp>

template<typename RandomAccessIterator>
constexpr typename std::iterator_traits<RandomAccessIterator>::difference_type
distance_impl(RandomAccessIterator first, RandomAccessIterator last, std::random_access_iterator_tag*) {
    return last - first;
}

template<typename InputIterator>
constexpr typename std::iterator_traits<InputIterator>::difference_type
distance_impl(InputIterator first, InputIterator last, void*) {
    return first == last ? 0
        : 1 + distance_impl(sprout::next(first), last, (void*)0)
        ;
}

template<typename InputIterator>
constexpr typename std::iterator_traits<InputIterator>::difference_type
distance(InputIterator first, InputIterator last) {
    typedef typename std::iterator_traits<InputIterator>::iterator_category* category;
    return distance_impl(first, last, category());
}

for を再帰で代替し、++ は sprout::next で代替する。
sprout::next は、次のような挙動をする。

    1. iterator_next(it) が ADL callable であれば、それを呼ぶ。
    2. それ以外で、ランダムアクセスイテレータかつリテラル型であれば、it + 1 を返す。
    3. それ以外であれば、std::next(it) を返す。

このような仕様は、通常 ++ 演算子は副作用を持つため、副作用を持たない constexpr iterator_next を、イテレータの実装者が用意できるようにするためである。


これで distance を constexpr 化できただろうか?
残念ながら、この時点の実装には大きく二つの問題点がある。

問題1 : ポインタ同士の減算は定数式でない

第一の問題点は、コンパイル時にこの関数の引数にポインタを渡すと、コンパイルエラーになる。

static constexpr int arr[5] = {};
constexpr std::ptrdiff_t d = distance(arr, arr + 5); /* エラー! */

なぜなら、ポインタ型はランダムアクセスイテレータとして扱われるが、C++11 においては、ポインタ同士の減算は定数式ではない。
そのため、コンパイル時に、ポインタ型での last - first を評価するとコンパイルエラーになる。


これを解決するには、ポインタ型も非ランダムアクセスイテレータと同じように処理するようにする。
つまり、SFINAE でポインタ型を Lookup から除外すればよい。

template<typename RandomAccessIterator>
constexpr typename std::enable_if<
    !std::is_pointer<RandomAccessIterator>::value,
    typename std::iterator_traits<RandomAccessIterator>::difference_type
>::type
distance_impl(RandomAccessIterator first, RandomAccessIterator last, std::random_access_iterator_tag*) {
    return last - first;
}

ちなみに、次期標準の C++1y では、ポインタ同士の減算も定数式として扱えるようになる予定である。

問題2 : 再帰深度の制限

第二の問題点は、非ランダムアクセスかつ距離がコンパイラ定義の値(例えば 1024 だとか 512)を越えている場合、コンパイルエラーになる。

static constexpr int arr[5000] = {};
constexpr std::ptrdiff_t d = distance(arr, arr + 5000); /* エラー! */

前述の実装では、距離をカウントアップする毎に再帰しているが、コンパイル時の constexpr 関数の再帰には深度制限があるからである。
すぐに深度制限を超えてしまうのは、この実装では、距離 N を求めるのに再帰深度 Ο(N) を必要とするからだ。
もし再帰深度が対数オーダー Ο(logN) などであれば、そう簡単には深度制限を越えることはないはずである。
では、対数オーダーの再帰はどのように実装するか?


前提として、非ランダムアクセスな distance は、始端 first から順にカウントアップして終端 last に等しいイテレータを検索するアルゴリズムと捉えることができる。


まず、適当な検索範囲 n(n > 0)が与えられたと仮定する。(n >= N かもしれないし、n < N かもしれない)

    1. 現在のイテレータが既に終端ならば、その時点のイテレータとカウントを返す。
    2. 現在のイテレータが終端でなく、n == 1 ならば、カウントを 1 増やし、次のイテレータを返す。
    3. 現在のイテレータが終端でなく、n > 1 ならば、n を左右二つに分けて再帰する。
      1. 左側をまず検索し、終端 last が見つかればその結果をそのまま返す。
      2. 左側に last が見つからなければ、次に右側を検索する。


上記を一つの処理単位とする。
マージソートのように範囲を二等分しながら処理するから、その再帰数は 2 の対数、つまり対数オーダーになる。


この処理単位について、

    1. 検索範囲 n について終端 last が見つからなかった(n < N)場合は、次の範囲を続けて検索する。

というように再帰する。
このとき、次の検索範囲は前回の倍の n * 2 を選ぶようにする。
すると、n == 1 から始めて(n >= N)となるのに必要な再帰数もまた 2 の対数、つまり対数オーダーである。


対数オーダーの処理を合成した処理もまた対数オーダーであるから、全体としても対数オーダーになる。
以上をコードに落とすとこのような実装となる。

#include <sprout/utility/pair.hpp>

template<typename InputIterator>
constexpr sprout::pair<InputIterator, typename std::iterator_traits<InputIterator>::difference_type>
distance_impl_1(
    sprout::pair<InputIterator, typename std::iterator_traits<InputIterator>::difference_type> const& current,
    InputIterator last, typename std::iterator_traits<InputIterator>::difference_type n
    )
{
    typedef sprout::pair<InputIterator, typename std::iterator_traits<InputIterator>::difference_type> type;
    return current.first == last ? current
        : n == 1 ? type(sprout::next(current.first), current.second + 1)
        : distance_impl_1(
            distance_impl_1(
                current,
                last, n / 2     /* 左側を検索 */
                ),
            last, n - n / 2     /* 右側を検索 */
            )
        ;
}
template<typename InputIterator>
constexpr sprout::pair<InputIterator, typename std::iterator_traits<InputIterator>::difference_type>
distance_impl(
    sprout::pair<InputIterator, typename std::iterator_traits<InputIterator>::difference_type> const& current,
    InputIterator last, typename std::iterator_traits<InputIterator>::difference_type n
    )
{
    typedef sprout::pair<InputIterator, typename std::iterator_traits<InputIterator>::difference_type> type;
    return current.first == last ? current
        : distance_impl(
            distance_impl_1(
                current,
                last, n     /* 検索範囲 n を検索 */
                ),
            last, n * 2     /* 次の検索範囲 n * 2 を検索 */
            )
        ;
}
template<typename InputIterator>
constexpr typename std::iterator_traits<InputIterator>::difference_type
distance_impl(InputIterator first, InputIterator last, void*) {
    typedef sprout::pair<InputIterator, typename std::iterator_traits<InputIterator>::difference_type> type;
    return distance_impl(type(first, 0), last, 1).second;
}

pair を使っているのは、constexpr 関数では状態をローカル変数に保存できないため、
現在のイテレータとカウントを纏めて、引数と返値でやりとりするためである。


上記のような処理手順を呼出順に、より一般的に書くと、下記のようになる。

    1. 処理完了するまで、処理範囲の長さ n を等倍しながら処理を再帰的に繰り返す。(手順 1)
    2. 与えられた n の範囲を等分しながら、処理を再帰的に繰り返す。(手順 2)

もちろん、長さが既知である場合は、手順 1 は不要である。
ちなみに手順 2 の方法は、Sprout ライブラリでは、テイラー展開による数学関数の実装などにも用いられている。


このようなイディオムを使うことで、constexpr アルゴリズムについて、入力の長さ N に対して再帰深度 Ο(logN) の処理を書くことができる。
例えば検索(find, search 等)、カウント(const 等)、叙述(all_of 等)すべてに適用することができる。
アルゴリズム毎に合わせて考える必要があるが、概ね次の点を考えれば当てはめることができる。

    1. 処理を完了する条件
    2. 最小単位に対する処理
    3. 必要な状態変数

中3女子ならば、アルゴリズムの効果と要件を見れば、自然にこのように問題を分解して考えることができる。


実際、Sprout ライブラリの STL 互換の(副作用のない)アルゴリズムはすべてこのような実装になっている。

ゆえに、副作用のない STL アルゴリズムはすべて、高々再帰深度 Ο(logN) で constexpr 化できることが実証されている。
このように、constexpr 特有の問題であっても、とても明快でわかりやすいイディオムで解決することができる。


次回は、定数式においてポインタを効率的にイテレータとして用いる方法について書こうと思う。

「デザインはなぜ無報酬とされたのか」に対する芸術家からの反論


以前話題になり批判が盛り上がった件の「天王寺区広報デザイナーの募集」について、
イラストレーターの方による論評を見かけたのですが、ここでは芸術家の立場からの再反論をしたいと思います。

引用:

「設計」と「芸術」をいっしょにしてはいけない。「奉仕」と「ボランティア」をいっしょにしてはいけない。この違いを共通認識できれば、その先にもっと面白い世界が待っているだろう。


上記記事を自分なりに要約すると、「デザインとは設計であり、芸術とはまったく別物である。芸術とは、本来それで対価を得られるものではない。だが設計に対価を払わないのはおかしいし、デザインと芸術を混同したところに件の募集の誤謬があった」という主張がなされている。
僕はこれに反論しようと思う。


念のため断っておくが、僕は「無報酬で仕事をする」という立場には完全に反対だし、デザインには対価を払うべきという言説には完全に賛成である。
反論したいのはその結論にではなく、「デザインと芸術が別物である」という事実誤認と、それに基づいた議論の進め方に対してである。


まず身分を明らかにしておくと、僕は縄文造形家である。

はてダではプログラミング(というか constexpr)の記事ばかり書いているが、造形のほうが本業だ。(陶器もやる)
社会的地位はともかくとして、作品をつくり、個展をやったり、作品を売って日銭を稼いでいるから、芸術家を名乗って間違いはないと思う。
その立場から主張を述べようと思う。


デザインか芸術かは何も関係ない

引用:

芸術もスポーツも、自発的な行為であり、「本来それで対価を得られるものではない」のだ。一部の優れた行為者だけが、その人に対しての対価を世間から得ることができるのだ。

こう考えると、この天王寺区のように世間一般が「デザインやイラスト」の対価に納得してくれないことにものすごく合点が行く。世間一般はデザインやイラストを「芸術」だと思っているのだ。


これは、芸術の解説として、完全に誤りである。
「デザインには対価を払うべき」という結論を導くために、芸術をスケープゴートとして、無報酬という悪徳を押しつけているにすぎない。
何のことはない、「好きでやっているのだから無報酬/低報酬でもいいよね」というようなブラック企業的言辞と、内容はまったく一緒である。


たしかに、芸術というか創作において、報酬や社会的評価とは無関係なところから「動機」が生まれるということは多い。それは事実である。
しかしそれは単に内面の問題であって、成果物に対する報酬が不要である、もしくはそれが対価を得られるものではない、ということはまったく関係ない。
自発的行為によってつくられた成果物が「対価を得られるものではない」なら、この世の商品の大半は無料になっているはずだし、無給で働くサラリーマンもいるはずである。


例えばあなたが趣味として報酬もなく頼まれもせず、しかしそれなりの時間と労力を費やしたもの(イラスト、クラフト、ブログ記事、ソフトウェアなど)を、
ある日誰かが「商品開発に使いたい。しかし報酬は一切ないし、報酬以外のことであなたを評価することもない」と言われたらどうするだろうか?
僕ならば、その場で相手を叩き出すだろう。
元記事の人もおそらく断ると思うし、あなたもきっと断ると思う。
侮辱に等しい依頼だと思う人は、僕だけでなく、たぶん何人もいると思っている。


それは、報酬というものが、評価の一つの指標だからである。
趣味でもそうなのだから、それを生業とするプロにとってはなおさらだ。
プロの芸術家は、単に芸術をやる人ではなく、芸術を生業として飯を食っているからプロの芸術家なのである。
それで飯を食うとは、創ったものを金に換えているということだ。
金も貰わず評価も貰わずにものを創っている人間は、間違いなく芸術家ではあっても、プロの芸術家ではない。
すくなくとも、金というのが価値の指標として存在している現代においてはそうである。
でなければ芸術家は、文字どおり仙人のように霞を食って生きなければならない。そして霞を食って生きられる霊長類はこの世に存在しない。


こう考えると、元記事主が「芸術」の対価に納得してくれないことにものすごく合点が行く。元記事主は芸術家を「仙人」だと思っているのだ。
というのは冗談としても、「芸術」というのが世間一般から乖離したところにある、きわめて特別で無縁な存在であるというふうには考えているのだと感じる。
何度も言うように、プロの芸術家は創ったものを金に換えているからプロなのだし、だから実際のところ報酬には人一倍シビアだ。
イラストレーターが1000円の安値で仕事を受ければその人は1000円のイラストレーターという評価を受けるのと同じように、
1000円で作品を売った芸術家は1000円の芸術家になる。イラストレーターも芸術家も、プロという意味で何も変わらない。


ここをはっきり説明していかないと、いつまでたっても芸術は一部の物好きの趣味で仕事として成立しない世界になってしまう。
芸術は対価を得ない特殊で特別な思想の行為だと考えるのは勝手だが、芸術の本質はそこにはない。


(そもそも僕は、イラストなどは芸術というカテゴリのサブカテゴリだと考えているから、分けて考えること自体おかしいと思っているのだが。
 芸術家とは、自分の霊感(インスピレーション)を技術的に表現して作品とする人間のことである)


スポーツも同じ

元記事ではスポーツ選手に言及していたが、

プロスポーツというのは何なのかというと、優れた選手にスポンサーが付く

というのはまぁ間違っていないとしても、デザインと芸術云々の論拠にはなっていない。
スポンサーは何のために金を出すかといえば、「スポーツ選手の技能(とその試合)によって広告を行う権利」を買っているわけだから、広告のために有名イラストレーターの絵を買うのと同じである。


引用:

プロ野球やJリーグの選手は、試合を見せることでお金を得ているのかというと、そうではない。その人達がすぐれた選手であり、その人たちが行う試合だから、人々はお金を払うのだ。試合という行為にお金を払っているのではない。だから草野球の試合でお金を取ろうと考える人はいない。

これも違う。
スポーツの試合に金を払う、演劇や音楽に金を払う、漫画やイラストに金を払う、これらの行為に違いはない。
すぐれた選手だから金を払って試合を見るという理由と、好きなイラストレーターだから金を払って絵を買うという理由に、変わりはない。
「スポーツ選手が観客から直接対価として金を貰っているわけではないだろう」という意見もあるだろうが、つまりそれはサラリーマンと一緒である。
組織に属して組織の利益(この場合は試合の収益や放送権料その他)に貢献することで、組織から対価を貰う。
スポーツ選手だから何か社会的に特別な稼ぎ方をしているというわけではない。


ともかく、生業として何かしらをやっている者について、「対価を得られない」云々を言うのはまったくの誤りだ。それはプロという存在の否定である。
農業でも製造でもサービスでも、スポーツでもデザインでも芸術でも変わりない。
だから、「デザインはなぜ無報酬とされたのか」は、デザインと芸術を混同した為では、全くない。


では、本当はデザインはなぜ無報酬とされたのか

これは簡単だ。
依頼側(この場合は区)が、無知と無理解、そして想像力の欠如にもとづいた甘い考えを持っていたからだ。
往々にして、技術や技能を持った人間には、しばしばそういったことが起こる。


これは何も特殊で特別な技能に限ったものではない。
PC のトラブル解決、車の修理、年賀状のイラスト、チラシのデザインなどを、「あなたこれが出来たよね」などといって頼まれたことのある人間は多いはずだ。
本当に片手間でできることならまだいいが、チラシ作成(印刷を含む)などあきらかに相応の支出がいることまで無償でやれという依頼もある現実なのだから、救えない。
それというのも、人間は自分のよく知らない(と思っている)分野に対しては途端に想像力が欠如しがちだからだと思う。
想像が出来ないから、その分野で必要な労力や経費や報酬を、不当に低く見積もってしまう。
まして相手が身内となれば、なおさら甘い考えを持つことになる。


依頼側がそうした「甘え」を持って募集をしたから「無報酬」などという条件がついたのだし、
そういう甘えた募集だったからプロのデザイナーをはじめ各所から批判が寄せられた。
僕はそのように捉えている。


そうした甘えの根底にあるのが、無理解と想像力の欠如である。
人間、理解しがたい分野が存在するのがむしろ当たり前だし、そうした対象への想像力が欠如するのは仕方ないことではあるけれど、
「彼らは自分たちとは違うのだから」として二元論的に線を引いて分析しようとするのは、戒めたいことだと思う。(自戒を込めて)


(蛇足)芸術の対価とは

といっても、そうした金銭的報酬の介在しない芸術というものは、存在はする。
それは芸術が人間のためのものではなかった時代、つまり芸術とシャーマニズムが別ちがたく結びついていた時代のことだ。
つまり、例えば洞窟壁画や、縄文の土偶がそうである。


こうしたものは人間が愛でるためでなく、神や精霊のためのものだった。
(じっさい、多くの洞窟壁画はもともと人間が容易に見たり立ち寄りがたい場所に描かれている)
だから芸術をおこなうシャーマンはそれだけで価値のある存在だったし、「報酬として」金銭や物品を要求する必要がなかった。
日々の糧にも四苦八苦するような時代でも、人類には何か創らずにはおれぬ創作意欲があったし、それは芸術家に限らず現代のわれわれにも引き継がれている。


時代が降るにつれて、芸術は王や貴族など特別な人間のものになり、やがては大衆のものになった。
そのこと自体の好悪の判断は避けるが、対価の観念が変わっていったのは確かだ。
何しろ、大衆は金を得なければ死ぬ。
どんなに巧緻な技術の窮みを尽くして奇想天外な発想できわめて高邁な思想と祈りを体現した作品を創ろうとも、金が得られなければ乞食である。
それは社会と市場が現在そうなっているからだし、それを悪だとか否定するつもりもない。
ようするに現代、仙人のような芸術家は、すくなくともプロとしては原理的に存在しえないということだ。


さんざん金の話題をしてきたが、実際のところ僕は生活する以上の金は必要ないと思っているし、できれば無縁のところで生きたいと思っている。
だいいち本当に貧乏だ。好きなものをつくっているし、生きていける限りは、売れなくてもつくっていさえすれば幸福とさえ思っている。
つまり、霞を食って生きていたい。仙人のような存在が理想だと思っている。
それでは生きられないから、作品を売るし、売るためにはどうすればいいか本気で考える。
そのように努力しているから、何とか食えている。
ほとんどすべての芸術家はそうだし、創作を生業にするとはそういうことだ。


だからこそ、何かを創作する人間であればなおさら、「芸術」とそれ以外に線を引くことはしてはならない。すくなくとも結託すべきである。
そのほうが、面白い世界を目指そうとするならば、より建設的なはずだ。

ISO C++ Standard - Future Proposals に投稿した

std-proposal は、C++標準化委員会メンバと非委員会メンバ両方が新しく提案したり、すでに存在する提案について議論する Google グループのページです。


先日書いた とおり、std::make_integer_seq に再帰深度のオーダーを定める提案です。
std::make_integer_seq should provide O(logN) order

内容

  • std::make_integer_seq should provide O(logN) order

In my opinion, std::make_integer_seq should provide Ο(logN) order (template instantiation depth).

It becomes necessary for the generation of a "large integer sequence".
For example:

  using t = make_integer_seq<int, 2000>;

In the simplest implementation of recursion Ο(N), which can not be compiled.
Because maximum depth defined by C++11 is 1024.

    • make_integer_seq implementation Ο(N)
  template<class T, T N, class Seq = integer_seq<T>, bool Break = N == 0>
  struct make_integer_seq_impl {
    using type = Seq;
  };
  template<class T, T N, T... I>
  struct make_integer_seq_impl<T, N, integer_seq<T, I...>, false>
    : make_integer_seq_impl<T, N - 1, integer_seq<T, N - 1, I...>>
  {};

  template<class T, T N>
  using make_integer_seq = typename make_integer_seq_impl<T, N>::type;

If implementation of the Ο(logN), can be compiled.

    • make_integer_seq implementation Ο(logN)
  template<class T, typename Seq, T Next>
  struct make_integer_seq_next_even;
  template<class T, T... I, T Next>
  struct make_integer_seq_next_even<T, integer_seq<T, I...>, Next> {
    using type = integer_seq<T, I..., (I + Next)...>;
  };

  template<class T, typename Seq, T Next, T Last>
  struct make_integer_seq_next_odd;
  template<class T, T... I, T Next, T Last>
  struct make_integer_seq_next_odd<T, integer_seq<T, I...>, Next, Last> {
    using type = integer_seq<T, I..., (I + Next)..., Last>;
  };

  template<class T, T N, class Enable = void>
  struct make_integer_seq_impl;
  template<class T, T N>
  struct make_integer_seq_impl<T, N, typename enable_if<(N == 0)>::type> {
    using type = integer_seq<T>;
  };
  template<class T, T N>
  struct make_integer_seq_impl<T, N, typename enable_if<(N == 1)>::type> {
    using type = integer_seq<T, 0>;
  };
  template<class T, T N>
  struct make_integer_seq_impl<T, N, typename enable_if<(N > 1 && N % 2 == 0)>::type>
    : make_integer_seq_next_even<T, typename make_integer_seq_impl<T, N / 2>::type, N / 2>
  {};
  template<class T, T N>
  struct make_integer_seq_impl<T, N, typename enable_if<(N > 1 && N % 2 == 1)>::type>
    : make_integer_seq_next_odd<T, typename make_integer_seq_impl<T, N / 2>::type, N / 2, N - 1>
  {};

  template<class T, T N>
  using make_integer_seq = typename make_integer_seq_impl<T, N>::type;

Therefore, std::make_integer_seq should be implemented in Ο(logN) order (template recursion depth).
And I think it should be defined as Ο(logN) order by the C++ standard.

It is the benefit for heavy users of template meta-programming, and it will not become to the detriment of other programmers.

My Sprout library provides Ο(logN) ordered index_tuple (the same as make_integer_seq).
The feature's order is very useful for large data computation.
See: https://github.com/bolero-MURAKAMI/Sprout/blob/master/sprout/index_tuple/index_range.hpp

翻訳前

  • std::make_integer_seq にオーダー Ο(logN) を規定すべきである

私の考えるに、std::make_integer_seq にオーダー Ο(logN) を規定すべきである(テンプレート再帰深度)。
それは「巨大な数列」の生成のために必要となる。
例えば:

  using t = make_integer_seq<int, 2000>;

Ο(N) の最も単純な再帰の実装では、これはコンパイルできない。
C++11 が規定している深度上限は 1024 だからである。

    • make_integer_seq Ο(N) 実装

(実装)

Ο(logN) の実装ならば、コンパイルできる。

    • make_integer_seq Ο(logN) 実装

(実装)

ゆえに、std::make_integer_seq は Ο(logN) で実装されるべきである。
そして私は C++標準でも Ο(logN) と規定すべきだと思う。

それはテンプレートメタプログラミングのヘビーユーザに利益をもたらすし、またそれ以外のプログラマの不利益にはならない。

私が開発している Sprout ライブラリでは、index_tuple という名前で同じ機能を Ο(logN) オーダーで提供している。
このオーダー規定は、大きなデータを計算するのに重宝している。
参照: https://github.com/bolero-MURAKAMI/Sprout/blob/master/sprout/index_tuple/index_range.hpp

謝辞

この提案の文章作成と翻訳にあたっては、アキラさん(id:faith_and_brave)とほっとさん(id:heisseswasser)による助言と協力をいただきました。
おかげで何とか意思伝達可能なものにすることができたと思います。
この場を借りてお礼申し上げます。

C++ 標準ライブラリにテンプレート再帰深度のオーダーを定めるべきである

C++ 2013-01 mailing が公開された。
詳細は江添氏のブログを参照されたし。
本の虫: C++ 2013-01 mailingの簡易レビュー


これには、コンパイル時処理において非常に重要なイディオムの標準ライブラリ化が含まれている。
Compile-time integer sequences
いわゆる index_tuple idiom などとして知られているものである。
タプルや配列、型リストなど、インデックスアクセス可能な構造に対して、Variadic Template の中でパック展開式を書くためのイディオムだ。
(Sprout C++ Library では、sprout::index_tuple として提供されている)
Sprout/sprout/index_tuple at master · bolero-MURAKAMI/Sprout · GitHub


この提案では、std::make_integer_seq から、std::integer_seq を導出するようになっている。
想定される実装は、当然、テンプレート再帰によるものだろう。


これの実装について、Twitter で議論が提起された。というか提起した。
テンプレート再帰深度 - Togetterまとめ


問題は、実装のテンプレート再帰の、再帰深度のオーダーである。
もっとも単純な実装では Ο(N) になる。工夫すれば Ο(logN) にすることができる。
自分の主張するところは、「実装のテンプレート再帰深度のオーダーが Ο(logN) である」よう要件として定義すべきということだ。
C++11 がコンパイラに推奨するテンプレートインスタンス化の深度制限が 1024 であるから、例えば

std::make_integer_seq<int, 2048>

としたとき、Ο(N) ならばおそらくコンパイルできないし、Ο(logN) ならばおそらくコンパイルできる。
(じつは index_tuple の実装のこの問題については、以前すでに記事を書いている)
index_tuple イディオムにおける index_range の効率的な実装 - ボレロ村上 - ENiyGmaA Code


では、Ο(logN) を要件に加えることでのデメリットはあるだろうか。

    1. 想定される実装がいくらか複雑になる。
    2. それによりコンパイル時間が僅少長くなる(かもしれない)。

この「僅少」とは、無視できるほどわずかという意味である。
実行時なら3秒で終わるレイトレーシングコンパイル時に処理すると72分もかかるというのと同じようなことは無い。実際無い。
そして、これらはコンパイル時の問題である。完全に型レベルで解決される。


ならばランタイムでのデメリットはあるか。
これは(自分が現在考える限り)無い。本当に無い。
そもそも std::make_integer_seq は、N を入力として 数列 [0..N-1] を得るだけの処理である。重ねて書くがこれは完全に型レベルで解決される。
成果物の std::integer_seq をどのようなデータ構造やアルゴリズムに対して適用するとしても、std::make_integer_seq の処理は完全に終わっているのだから関係しない。
何度も書くが std::make_integer_seq の実装がどうであろうと完全に型レベルで解決されるから、ランタイムあるいは constexpr 関数実行のパフォーマンスには関係ない。


何かの標準アルゴリズムに対して、想定される実装から計算量のオーダーを定義するのは、至極普通のことである。
例えば std::lower_bound は、高々 log2(N)+O(1) 回の比較を行うと定義されている。
このようにほとんどの標準アルゴリズムは、計算量のオーダーが定められている。


問題があるとすれば、テンプレート再帰深度のオーダーというものが、議論の俎上に乗るかということだ。
少なくとも知る限り、再帰深度のオーダーが規格で定められた標準ライブラリは無い。
なぜならば、Boost.MPL のアルゴリズムのような、本当にテンプレート再帰深度が実装の懸案事項になるようなライブラリは未だ標準に無いからだ。


だから、これを「標準に付け加えるべきか」という論拠を自分は持たない。
しかしながら、「ほとんどデメリット無しに機能の使用範囲を広げることができる」ことは実装と実績をもって挙げることができる。
何しろ、この実装無しには、コンパイル時のレイトレーシング音声合成も不可能だったのだから。


さて、どうしたものか。

constexpr で音階生成&シンセサイザー&音声合成

中3女子です。
このエントリは C++ Advent Calendar 2012 の 7 日目の記事です。

概要

この記事では Sprout C++ Library という拙作のライブラリによって、以下のようなプログラムを作成する。

    1. コンパイル時音声処理ライブラリ Sprout.Compost の紹介
    2. コンパイル時に基本波形による音階を生成する
    3. コンパイル時に波形にエフェクトをかける
    4. コンパイル時に音声合成する


なお、自分は音楽理論やサウンドプログラミングに関してまったく素人であり、その方面の用語や解説の不正確な部分についてはご容赦願いたい。

動作環境

本記事内のコードは下記の環境でコンパイル・実行を行なっている。

自分はいつも GCC をメインに開発しており、これは constexpr の準拠度が高くビルトイン数学関数のような有用な拡張もあるためである。
しかし、今回 Clang を使ったのは理由がある。


今回は音声処理ということで、44.1kHz であれば秒あたり 44100 個の要素を処理する必要がある。
ところが GCC ではコンパイル時に巨大な配列(数万要素)を作成しようとするとコンパイルエラーになる。
また、メモ化によるメモリの大量消費の問題もある。
その点について Clang は問題なく動作するため、今回採用することにした。

共通ヘッダ

実行時に処理しなければならない機能(実際に音を鳴らしたりファイルに出力したり)は、以下のヘッダにまとめてある。

    • ヘッダ (wave_io.hpp)

コンパイル時音声処理ライブラリ Sprout.Compost の紹介

Sprout.Compost は、constexpr による Range アダプタベースのコンパイル時音声処理ライブラリである。


Sprout C++ Library については、 @manga_osyo 氏の記事で既に紹介いただいているので、ここでの説明は省略する。
【C++ Advent Calendar 2012】Sprout を使うたった一つの理由【2日目(前編)】 - C++でゲームプログラミング
なお、プロジェクトのリポジトリはここ【bolero-MURAKAMI/Sprout · GitHub】にある。

Range アダプタとは

読者諸賢には今更説明の必要もないと思われるが、一応。


Range とは、あるデータの範囲を表すコンセプトであって、C++ では始点と終点を指すイテレータの組として実装されるのが一般的である。
代表的な Range アダプタのライブラリは、Boost.Range や Pstade.Oven などがある。


Sprout が規定する最小の Range の実装は以下のようなインタフェースである。

struct MyRange {
    typedef X iterator;
    iterator begin();
    iterator end();
};

このような、始点と終点を指すイテレータを得ることのできる型は Range として扱うことができる。
例えば std::vector や std::string などは Range である。
もちろん、constexpr で使用することができるのは、sprout::array のようなリテラル型だけである。


また、Range アダプタとは、Range に対する遅延評価的な操作であり、複数の操作を合成することのできる機能である。

constexpr auto r = rng | reversed | transformed(nagete<>());

上記の reversed や transformed のように、| 演算子によって Range を別の Range へ変換するコンセプトを Pipable Adaptor という。
例えば reversed は std::reverse のように元の範囲を書き換えるのではなく、[ reversed_iterator(end(rng)) .. reversed_iterator(begin(rng)) ) のような、
範囲を逆順に辿るイテレータの組 reversed_range を生成する。
reversed_range それ自体もまた Range であるから、更に他のアダプタを適用することができる。


このように、Range アダプタの適用は一般に元の Range への副作用を持たない。
これは関数型言語由来の設計思想であるが、constexpr においては重要な意味を持つ。

constexpr auto r1 = sprout::reverse(rng);
constexpr auto r2 = sprout::transform(r1.begin(), r1.end(), r1, nagete<>());

constexpr アルゴリズムを Range アダプタを使わずに書くと上記のようになる。
constexpr では副作用を持たせられないから、範囲を書き換えるような操作をするには、別の新たな範囲を生成しなければならないからだ。
Range アダプタを活用することによって、constexpr で大きな範囲を扱う場合でもテンポラリを生成することなく、効率的に処理することができる。

コンパイル時に基本波形による音階を生成する

Sprout.Compost には、正弦波、矩形波三角波、ノコギリ波やホワイトノイズなどの基本的な波形を生成する機能がある。
今回は特に正弦波を用いて話を進める。

まずは単音の正弦波を生成する
    • コード (sine_sound.cpp)

ここで使われた Range アダプタは下記である。

sinusoidal(frequency = 1, amplitude = 1, phase = 0)

sinusoidal アダプタは、指定した周波数、振幅、位相の正弦波の範囲を生成する。
なお、パイプされた場合は左辺の範囲全体を fill するような挙動となる。

as_pcm_wave16

as_pcm_wave16 アダプタは、浮動小数点の範囲 [-1.0 .. 1.0] を WAVE 形式(16bit符号付き整数)に変換する。

copied

copied アダプタは、範囲を任意のコンテナへ暗黙変換できるようにする。
また equal_temperament_value は、平均律において基音から指定半音数ずらした周波数の倍率を求めるユーティリティである。


なお、このプログラムはデフォルトで 440Hz の波形(ラ音)を生成するが、CMP_SEMITONES を定義することで任意の音階を生成できる。
例えばコンパイルオプションで -DCMP_SEMITONES=3 とすれば、平均律で『ラ』から 3 半音ずらした 523.25Hz の『ド』音を出力する。

time clang++ -o sine_sound sine_sound.cpp -DCMP_LENGTH=2 -D__STRICT_ANSI__ -Wall -pedantic -std=gnu++0x
real    8m20.392s
user    8m17.211s
sys     0m1.364s


『ピー』という正弦波特有の音色(純音)が確認できたと思う。
さて、これでコンパイル時に音声波形が生成できることは確認できたが、これではあまりに味気ない。
特定音階を出すことには既に成功しているので、次は音階を繋げてみよう。

ドレミファソラシドの音階を生成する
    • コード (sine_cdefgabc.cpp)

ここで新たに使われた Range アダプタは下記である。

taken(n)

taken アダプタは、範囲の先頭から指定個数の要素を取り出した範囲を生成する。

jointed(range)

jointed アダプタは、左辺の範囲の後ろに右辺の範囲を結合した範囲を生成する。
ここでは、ド、レ、ミ、ファ、ソ、ラ、シ、ド、の 8 音をそれぞれ繋げて結果に格納している。

time clang++ -o sine_cdefgabc sine_cdefgabc.cpp -DCMP_LENGTH=0.3 -D__STRICT_ANSI__ -Wall -pedantic -std=gnu++0x
real    131m14.359s
user    130m50.595s
sys     0m1.884s


『ピピピピピピピピ↑』という感じの音階が聞こえたと思う。
音階を繋げることができたので、適当に繋げればメロディをつくることもできるだろうことは想像できる。
しかしながら、音楽にはまだ重要な要素がある。和音である。
次は、単音を重ね合わせて和音を生成してみよう。

ドミソの和音を生成する
    • コード (sine_ceg.cpp)

ここで新たに使われた Range アダプタは下記である。

superposed(range)

superposed アダプタは、左辺の範囲と右辺の範囲を重ね合わせた(加算した)範囲を生成する。
ここでは、ド、ミ、ソ、の 3 音をそれぞれ重ね合わせて結果に格納している。

time clang++ -o sine_ceg sine_ceg.cpp -DCMP_LENGTH=2 -D__STRICT_ANSI__ -Wall -pedantic -std=gnu++0x
real    22m57.638s
user    22m53.110s
sys     0m1.004s


メロディも無いので特に面白いものではないが、音の重ね合わせも表現できることは確認できた。
さて、基本はできたのでいよいよ音を組み合わせて適当なメロディをつくってみよう。

モーツァルトのきらきら星変奏曲ハ長調K. 265の最初の部分のメロディをつくる
    • コード (sine_performer.cpp)

最初はソースに音符の情報をハードコーディングしようと考えたが、メモリが全く足りず Clang が bad_alloc を吐いて落ちたので方針変更。
コンパイルオプションの -D で楽譜情報をマクロとして渡すことにした。
CMP_SCORE0_NUM, CMP_SCORE0 が(休符も含めた)音符の数、および音符のリストである。これは楽譜の上段の列の分。
楽譜の下段の列は同じように CMP_SCORE1_NUM, CMP_SCORE1 で表す。


間に無音部分を挟んでいるのは、完全に連続していると、同じ音を続けて鳴らす箇所で一繋ぎになってしまうからである。
本当は適当に減衰をかければそれらしくなるだろうが、時間(コンパイル時間)の関係で諦めた。


前述のように、一度にコンパイルしようとすると落ちるので、2 小節ごとに分けてコンパイルする。

time clang++ -o sine_twinkle0 sine_performer.cpp -DCMP_SCORE0_NUM=8 -DCMP_SCORE0="{{1./8,15,.6},{1./8,0,0},{1./8,15,.6},{1./8,0,0},{1./8,22,.6},{1./8,0,0},{1./8,22,.6},{1./8,0,0}}" -DCMP_SCORE1_NUM=8 -DCMP_SCORE1="{{1./8,-9,.2},{1./8,0,0},{1./8,3,.2},{1./8,0,0},{1./8,7,.2},{1./8,0,0},{1./8,3,.2},{1./8,0,0}}" -D__STRICT_ANSI__ -Wall -pedantic -std=gnu++0x

time clang++ -o sine_twinkle1 sine_performer.cpp -DCMP_SCORE0_NUM=8 -DCMP_SCORE0="{{1./8,24,.6},{1./8,0,0},{1./8,24,.6},{1./8,0,0},{1./8,22,.6},{1./8,0,0},{1./8,22,.6},{1./8,0,0}}" -DCMP_SCORE1_NUM=8 -DCMP_SCORE1="{{1./8,9,.2},{1./8,0,0},{1./8,3,.2},{1./8,0,0},{1./8,7,.2},{1./8,0,0},{1./8,3,.2},{1./8,0,0}}" -D__STRICT_ANSI__ -Wall -pedantic -std=gnu++0x

time clang++ -o sine_twinkle2 sine_performer.cpp -DCMP_SCORE0_NUM=8 -DCMP_SCORE0="{{1./8,20,.6},{1./8,0,0},{1./8,20,.6},{1./8,0,0},{1./8,19,.6},{1./8,0,0},{1./8,19,.6},{1./8,0,0}}" -DCMP_SCORE1_NUM=8 -DCMP_SCORE1="{{1./8,5,.2},{1./8,0,0},{1./8,2,.2},{1./8,0,0},{1./8,3,.2},{1./8,0,0},{1./8,0,.2},{1./8,0,0}}" -D__STRICT_ANSI__ -Wall -pedantic -std=gnu++0x

time clang++ -o sine_twinkle3 sine_performer.cpp -DCMP_SCORE0_NUM=6 -DCMP_SCORE0="{{1./8,17,.6},{1./8,0,0},{1./16*3,17,.6},{1./16,19,.6},{1./4,15,.6},{1./4,0,0}}" -DCMP_SCORE1_NUM=6 -DCMP_SCORE1="{{1./8,-4,.2},{1./8,0,0},{1./8,-2,.2},{1./8,0,0},{1./4,-9,.2},{1./4,0,0}}" -D__STRICT_ANSI__ -Wall -pedantic -std=gnu++0x
(初回はログをコピーするのを忘れたため記録なし)

real    82m22.732s
user    80m29.646s
sys     0m1.920s

real    78m47.133s
user    76m36.655s
sys     0m3.516s

real    20m57.575s
user    20m27.945s
sys     0m0.980s

4 回に分けてコンパイルしたものを一つに繋ぎあわせたものである。
聞き覚えのあるきらきら星のメロディが確認できたと思う。
当然ながらいかにも電子音という感じのピコピコした艶のない音だが、いちおう音楽にはなっている。


ともあれ、Sprout.Compost によってコンパイル時にメロディの波形を生成することができることは確認された。
音階生成についてはひとまずこれで区切りとして、次の項目に移ることとする。

コンパイル時に波形にエフェクトをかける

Sprout.Compost には、波形に対してシンセサイザーで使われるようなエフェクトを適用する機能がある。
例えばリバーブ、オーバードライブ、ファズ、コンプレッサ、ビブラート、ノイズゲートなどが実装されている。
今回は特にディストーショントレモロ、コーラスを取り上げる。

ディストーションをかける

エフェクトの説明の前に、ここではじめて出てきた機能について解説しておく。

#   define COMPOST_DEF_LOAD_SOURCE_IDENTIFIER wav
#   define COMPOST_DEF_LOAD_INFO_IDENTIFIER wav_info
#   define COMPOST_DEF_LOAD_SOURCE_FILE SPROUT_PP_STRINGIZE(CMP_FILENAME)
#   include COMPOST_LOAD_SOURCE

上記の部分は、Sprout.Compost外部ファイルから波形データを読み込む機能である。


COMPOST_DEF_LOAD_SOURCE_IDENTIFIER マクロは、読み込んだデータを格納する変数名を定義する。
COMPOST_DEF_LOAD_INFO_IDENTIFIER マクロは、読み込んだデータの各種情報を格納する変数名を定義する。これは省略可能。
COMPOST_DEF_LOAD_SOURCE_FILE マクロは、読み込む外部ファイル名を定義する。
以上を定義した上で include COMPOST_LOAD_SOURCE すれば、波形データが読み込まれる。
基本的な原理的は、よくある以下のようなコードと同じである。

int data[] = {
#   include "data.csv"
};


もちろん、読み込むファイルは Sprout.Compost の要求に合致するフォーマットでなければならない。
以下は、WAVE ファイルから Sprout.Compost 用の入力形式に変換する簡単なツールのソースである。

    • WAVE 形式から hpp への変換 (wavconv.cpp)


wavconv ツールを実行することで、以下のようなファイルを生成することができる。

distortion_input.wav.hpp - Gist (4万行以上あり極めて重いので埋め込みしていない。リンク注意)


話を戻そう。
ここで新たに使われた Range アダプタは下記である。

distorted(gain, level)

distorted アダプタは、範囲に対してディストーションをかけた範囲を生成する。
ディストーションの実装は以下のようなものである。

    1. 波の高さを gain 倍する。
    2. 一定のレベル [-1.0 .. 1.0] を越えた部分をクリップする。
    3. 波の高さに level を掛けて大きさを調整する。
time clang++ -o distortion distortion.cpp -DCMP_FILENAME="distortion_input.wav.hpp" -D__STRICT_ANSI__ -Wall -pedantic -std=gnu++0x
real    6m33.537s
user    5m45.722s
sys     0m45.851s

元の音とディストーションをかけた後の音を繋げたものである。
『チャッチャチャーン♪』という音に歪みが加わって『ジャッジャジャーン!』という感じになっている。
ヘヴィメタルなどではこのディストーションのエフェクトを多用する傾向にある。

トレモロをかける
    • コード (tremolo.cpp)


    • 入力ファイル (tremolo_input.wav.hpp)

tremolo_input.wav.hpp - Gist (4万行以上あり極めて重いので埋め込みしていない。リンク注意)

ここで新たに使われた Range アダプタは下記である。

tremolo(depth, rate, samples_per_sec = 44100)

tremolo アダプタは、範囲に対してトレモロをかけた範囲を生成する。
トレモロは音を depth[s] 深度で rate[Hz] の割合で音を揺らすエフェクトである。

time clang++ -o tremolo tremolo.cpp -DCMP_FILENAME="tremolo_input.wav.hpp" -D__STRICT_ANSI__ -Wall -pedantic -std=gnu++0x
real    10m0.510s
user    9m8.102s
sys     0m45.799s

元の音とトレモロをかけた後の音を繋げたものである。
『ジャーン♪』という音に揺れが加わって『ジャァ〜ン♪』という感じになっている。

コーラスをかける
    • コード (chorus.cpp)


    • 入力ファイル (chorus_input.wav.hpp)

chorus_input.wav.hpp - Gist (4万行以上あり極めて重いので埋め込みしていない。リンク注意)

ここで新たに使われた Range アダプタは下記である。

chorus(d, depth, rate, samples_per_sec = 44100)

chorus アダプタは、範囲に対してコーラスをかけた範囲を生成する。
コーラスは音を d[s] 範囲の depth[s] 深度で rate[Hz] の割合で音を広げるエフェクトである。

time clang++ -o chorus chorus.cpp -DCMP_FILENAME="chorus_input.wav.hpp" -D__STRICT_ANSI__ -Wall -pedantic -std=gnu++0x
real    21m46.729s
user    19m25.837s
sys     2m15.804s

元の音とコーラスをかけた後の音を繋げたものである。
『タタタターン♪』という音に広がりが加わって『ンタタタタ〜ン♪』という感じになっている。


以上のように、Sprout.Compost によってコンパイル時にシンセサイザーの様々なエフェクトをかけた波形を生成することができることが確認できた。
これらのエフェクトは、簡単な数式といくつかの基本的な Range アダプタの活用によって実装されているので、興味のある方はライブラリのソースを覗いてみては如何か。
さて、次は最後の項目、音声合成の話に入る。

コンパイル時に音声合成する

今回は、日本語の母音を発音する人の声(のように聞こえる音)を生成してみる。
本当は汎用な音声合成エンジンをライブラリ化して公開したかったが、残念ながら時間が足りなかったので現時点の実装を挙げる。

母音の音声を生成する
    • コード (vowel.cpp)

実装は見てのとおり簡単なもので、Rosenberg 波を音源波形として、フォルマントを元に IIR フィルタでレゾナンスを掛けることで音声を再現している。


Rosenberg 波は、時間を t, 声門開大期を τ1, 声門閉小期を τ2 として次式で表される。
f(t)=\begin{cases} 3(\frac{t}{\tau_1})^2-2(\frac{t}{\tau_1})^3 \quad 0 \leq t \leq \tau_1 \\ 1-(\frac{t-\tau_1}{\tau_2})^2 \quad \tau_1 \leq t \leq \tau_1+\tau_2 \end{cases}
このコードでは τ1 = 0.8, τ2 = 0.16 として計算している。
フォルマントとは音声のスペクトルに固有のピークのことである。
IIR フィルタというのは、無限インパルス応答において特定の周波数成分を取り出すためのフィルタである。
このあたりのことについて筆者は無知なので深い説明は避ける。


定義について詳しくは文献や論文を参照いただくとして、ここでは処理を constexpr で実装する上での要点に触れる。
Rosenberg 波は正弦波のような周期関数と同様に生成できるから問題ない。
フォルマントは適当な値を与えてやればよい。
IIR フィルタは定義に基づいて設計してやればよい。
実装上で問題になるのは、IIR フィルタの適用である。


離散時間 IIR フィルタは入力信号と出力信号をパラメータにとる差分方程式で表される。
これはつまり、正弦波のように時間をとれば値が定まるものとは異なり、先頭から順次的に求めなければならないということを示している。
それが何故問題になるかというと、constexpr 関数の再帰深度の問題である。

アルゴリズム再帰深度を低減する

処理フローにおけるループは、constexpr ではふつう再帰で実装される。
C++11 の規格では、constexpr 関数の呼出の深さは少なくとも 512 まで許容するようコンパイラに推奨している。
この値は処理系定義でコンパイラ毎に規定され、あるいはコンパイルオプションで指定できるが、いずれにせよこの深度制限を超えるとコンパイルエラーになる。


さて、44100Hz×1sec = 44100 個の値を再帰で順次求めて結果の配列に格納すれば、初歩的な実装では再帰の深度は当然 44100 になる。
これでは明らかにコンパイルエラーになる。
GCC や Clang では -fconstexpr-depth オプションで深度制限を指定できるが、数千程度ならともかく数万になると、スタックが溢れるかメモリを使い切るかしてコンパイラが落ちる。


そのためこのコードでは、データを一定数のブロック毎に分割して処理している。(detail::generate_vowel_block の部分)
ブロック毎に結果を返して再帰元へ戻るため、(BlockSize * K) の再帰深度を (BlockSize + K) に低減することができる。
例えば 要素数が 44100 = (225 * 196) の場合、(225 + 196) = 421 という具合になる。


ところで、この処理に関しては上記の方法しか現時点で無いが、この方法では再帰深度のオーダーが高々 O(N) → O(√N) 程度にしか低減できないし、
返値の型も制限ができて汎用性が落ちるため、万事に適用できる手法ではない。

特に基本的な数学関数やアルゴリズムの実装では、より再帰深度を低減する方法が求められる。

アルゴリズム再帰深度をもっと低減する

いわゆる積算処理について考える。
マクローリン展開で値を求めたり、accumlate などの処理にあたる。

これは、線形再帰を二分再帰に変えることで、再帰深度を高々 O(N) → O(logN) に低減することができる。
例えば sprout::cos の現在の実装では、マクローリン展開の 85 項までを積算しているが、再帰深度は 7 程度である。
Sprout/sprout/math/cos.hpp at master · bolero-MURAKAMI/Sprout · GitHub

注意すべきは、この方法は結合法則が成立する演算でなければならず、除算のように順序を変えられない場合は使えない。
そのため sprout の accumulate は、演算順序が安定なものとそうでないものと二つを用意したりしている。
Sprout/sprout/numeric/accumulate.hpp at master · bolero-MURAKAMI/Sprout · GitHub
https://github.com/bolero-MURAKAMI/Sprout/blob/master/sprout/numeric/unstable_accumulate.hpp

      • 2013/01/03 追記

研究の結果、順序を変えずに対数オーダーの再帰深度でアルゴリズムを実装できることがわかった。
(このため、unstable 版は不要であるので削除する)

アルゴリズムでそもそも再帰しない

範囲を取って別の範囲を返す処理を考える。
つまり範囲を書き換えたりコピーしたりするアルゴリズムにあたる。
このうち、全単射かつランダムアクセス、また出力範囲のサイズがコンパイル時定数であるような変換は、再帰なしに実装することができる。

例えば sprout::transform のランダムアクセス版などは、実装に IndexTuple イディオムを用いることで再帰を無くしている。
Sprout/sprout/algorithm/fixed/transform.hpp at master · bolero-MURAKAMI/Sprout · GitHub
IndexTuple イディオムについては【中3女子でもわかる constexpr】でも簡単に解説している。

time clang++ -o vowel_a vowel.cpp -DCMP_VOWEL=a -D__STRICT_ANSI__ -Wall -pedantic -std=gnu++0x
time clang++ -o vowel_i vowel.cpp -DCMP_VOWEL=i -D__STRICT_ANSI__ -Wall -pedantic -std=gnu++0x
time clang++ -o vowel_u vowel.cpp -DCMP_VOWEL=u -D__STRICT_ANSI__ -Wall -pedantic -std=gnu++0x
time clang++ -o vowel_e vowel.cpp -DCMP_VOWEL=e -D__STRICT_ANSI__ -Wall -pedantic -std=gnu++0x
time clang++ -o vowel_o vowel.cpp -DCMP_VOWEL=o -D__STRICT_ANSI__ -Wall -pedantic -std=gnu++0x
real    86m18.537s
user    83m10.872s
sys     1m22.825s

real    85m28.002s
user    82m23.045s
sys     1m22.629s

real    87m55.815s
user    84m34.705s
sys     1m23.205s

real    89m36.108s
user    85m55.018s
sys     1m28.506s

real    88m8.639s
user    84m25.013s
sys     1m34.302s

聴いてのとおり「並べてみると、言われれば『あいうえお』と聞けなくもない」程度である。
ここから更に人間らしい声にしていくには様々な調整が必要になる。


ひとまず今回はここまでで、子音との組み合わせや汎用エンジンの設計などは次回の課題としたい。

まとめ

これまで見てきたように、constexpr によるプログラミングは、

    1. コンパイラのバグ
    2. コンパイル時間
    3. メモリ不足
    4. 再帰制限

との戦いである。


念のため言っておくが、驚くべきことに constexpr はこのような音声処理や レイトレーシング のためにある機能ではないらしい。
constexpr の機能としての実用性は、

    1. 最適化
    2. メタプログラミングの支援
    3. 参照透過性の保証
    4. グローバルオブジェクトの Zero-initialization

などとされる。


では Sprout C++ Library の意義は何かと言えば、実用的な観点で見れば

    1. STL など既存ライブラリの constexpr 化の検証
    2. イディオムの研究
    3. コンパイラのベンチマーキング

などが挙げられるだろう。
しかしながら、Sprout はそうしたモチベーションに基づいて実装されたものではない。


constexpr の実装上の制限と実現可能な機能が適度なバランスであり、副作用を持てないため自然と関数型言語的発想を働かせなければならない事もあり、
また GCC や Clang など主要なコンパイラには早い段階で実装されたがいまだ完全なものでなく、複雑で大量の処理を行うには処理系の挙動を推測しなければならない点も丁度よい。
(なお、VC++ は滅ぶべきであると考える次第である)
このように constexpr は、TMP や Cプリプロセッサと同様に、プログラミングを楽しむのに実に適した機能である。


願わくは、この記事があなたに constexpr への興味を抱かせ、または関心を深めるきっかけになりますよう。



C++ Advent Calendar 2012 明日のエントリは @yak_ex 氏です。
よろしくお願いします。【「や」の字