はじめに
みなさんはC++11のconstexprを覚えているだろうか。そもそもその頃はC++を知らなかった人もいるかもしれない。改めてcpprefjpを眺めてみよう。
C++11のconstexprは極めて強烈な制約があった。つまり、実質的にconstexpr関数はreturn文しかかけず、条件分岐は三項演算子で行い、ループは再帰で実現していた。しかもその再帰は512回までしか保証がない。
動的メモリー確保すらコンパイル時に行えるこんにちから考えれば信じられないことである。
このころのconstexprを知る猛者たちはみな三項演算子やら再帰の入れ子に慣れ親しんでいる。しかし時がたつにつれそんな猛者たちも次第に記憶が薄らいでいるだろう。この5年のC++の進化はすさまじいものだったからだ。
この記事はその頃に思いをはせようかと思う。
・・・などとそれらしい前書きをでっちあげてみたが、実のところVS2015でコンパイル時処理をする需要が発生したから書いたコードを供養するのが目的である。
C++11時代のconstexprな処理を書くのに実質必須だったもの
強烈な制約を乗り越えるための方法が編み出されたが、非常に長大で整理困難なコードを要求する。そこで事実上必須だったのがSproutだ。
このライブラリは中3女子ことボレロ村上氏(本名: 村上原野)によって書かれた。コンパイル時処理をするときに欲しいなと思うものは大抵すべてここにある。msvcのテストケースになったりなどなどMSの公式ブログからも3度言及されていた。
- https://devblogs.microsoft.com/cppblog/expression-sfinae-improvements-in-vs-2015-update-3/
- https://devblogs.microsoft.com/cppblog/constexpr-and-aggregate-initialization/
- https://devblogs.microsoft.com/cppblog/profiling-template-metaprograms-with-cpp-build-insights/
残念ながら作者は2020年の2月に亡くなっている
彼の遺作に今回もお世話になろう。
なお彼の婚約者だった人が彼の縄文土器の作品集を作るべくクラウドファンディングをしているので興味がある人は探してみてほしい。
sprout::string<N>
コンパイル時に動的確保ができなかった時代、どうやって文字列をコンパイル時に取り扱うか、それはstd::array
のように構造体に配列を持たせたようなデータ構造によって実現された。
要素コピーはどうするかというとindex_tupleイデオムによって達成していた。なぜならばfor文を書けないからだ。
こうした苦労をsprout内部で遮蔽してくれるおかげで
template<std::size_t N>
constexpr sprout::string<N - 1> foo(const char (&str)[N])
{
return sprout::to_string(str);
}
のように書くことができる。実にわかりやすい。
sprout::all_of
では要素探索はどうすればいいのか。魂が抜けそうになりながら再帰をしなければならないのか?
sprout::all_of
を使えばできる。しかも再帰上限をいい感じに遠ざけてくれる。
作ったもの
# include <boost/utility/string_ref.hpp>
# include <cstddef>
# include <iostream>
# include <sprout/algorithm/all_of.hpp>
# include <sprout/string.hpp>
# include <stdexcept>
struct PredIsUpperChar
{
constexpr bool operator() (const char c) const noexcept
{
return !(u8"a"[0] <= c && c <= u8"z"[0]);
}
};
template<std::size_t N>
struct AssumeIsUpperCaseStr
{
sprout::string<N> str;
};
template<std::size_t N>
constexpr AssumeIsUpperCaseStr<N - 1> AssumeUpperCase(const char (&str)[N])
{
return sprout::all_of(sprout::begin(str), sprout::end(str), PredIsUpperChar{})
? AssumeIsUpperCaseStr<N - 1>{ sprout::to_string(str) }
// constexpr関数の中で例外を投げることで、コンパイルエラーにする。
// gcc,clangのような賢いコンパイラならこのthrow文まるごと表示してくれるが
// msvcの場合でも行数は出してくれるからそれを見に行けばわかる
: throw std::invalid_argument("input string must be UpperCase");
}
void bar(boost::string_ref l, boost::string_ref r)
{
std::cout << "sr:" << std::boolalpha << (l == r) << std::endl;
}
template<std::size_t N>
void bar(boost::string_ref l, const AssumeIsUpperCaseStr<N>& r)
{
std::cout << "foo:" << std::boolalpha << (l == boost::string_ref(r.str.c_str(), r.str.size())) << std::endl;
}
// 文字列リテラルを直接渡すのをblockする
template<std::size_t N>
void bar(boost::string_ref l, AssumeIsUpperCaseStr<N>&& r) = delete;
static constexpr auto expected1 = AssumeUpperCase("AAA");
//static constexpr auto expected2 = AssumeUpperCase("aaa"); // => note: 'std::invalid_argument::invalid_argument' の使用量を参照してください
int main()
{
bar("AAA", expected1);
//bar("AAA", AssumeUpperCase("AAA")); // => error
//bar("AAA", expected2);
bar("AAA", "AAA");
}
様々な事情からboost 1.59を使っている。今は亡きboost::string_ref
を眺めて懐かしんでほしい。
なおVS2015のIntelliSenseは爆発四散してしまう
いろいろあって使わないことになったので供養。