エラー読んでみた。コンパイル時計算してみた。
CCS 2021アドベントカレンダー表面 18日目の記事です。
間違って消してしまい、コピペして直してました。申し訳ございません。
12/18 16:01 一部修正
12/18 16:07 一部修正
前日の記事はこちら
https://note.com/glvntla/n/n6f960798ff6a
初めまして、CCS 21のStew(シチュー)と申します。
皆さん自然な流れで導入を書いてますね。すごい。私には無理そうなので、「全人類C++に飢えている」と仮定して記事を書き始めようと思います。
注意:
筆者はこの記事の執筆にあたり、特に下調べなどをせずに書いています。誤りや未定義動作が含まれる可能性は非常に高いです。見つけたら教えていただけると幸いです。
目次
1. コンパイルメッセージと仲良くなろう
2. コンパイル時計算は意外と簡単?
1. コンパイルメッセージと仲良くなろう
頑張って書いたコードを実行したらエラーがたくさん出た…。
こうなるとやる気が失せる。とてもわかる。
しかし、その大量のエラーメッセージはコンパイラがあなたのためを思って一生懸命に出力してくれたものだ。
エラーメッセージを有効活用できるようになれば、あなたのプログラミング人生はより快適になるだろう。
以下のページを参考にした。ここ読み切ればなんでもできるようになるんじゃないかな。
江添亮のC++入門
https://ezoeryou.github.io/cpp-intro/
1.1 エラーと警告の違い
C/C++においてエラー(error)と警告(warning)は異なるモノを意味する。
エラーは、明確に文法が間違っている場合にコンパイラが指摘するものだ。
対して警告は、文法的にはあっているが書き換えたほうがいいかもしれない場合にコンパイラが教えてくれるものだ。
1.2 コンパイルメッセージを読んでみよう
では実際にコンパイルメッセージを読んでみよう。
以下のコードをコンパイルしてみる。コンパイラはClangを使った。
#include <iostream>
print(char * s)
{
printf("%s",s);
}
int main()
{
std::cout << "Hello,World!" << endl;
print("Make a lot of error!");
}
以下はctrl+shift+Bした結果コンソールに表示されたものだ。(パスは弄った)
"C:/Program Files/LLVM/bin/clang++.exe" -std=c++14 -Wall -pedantic-errors -fconstexpr-depth=-1 -fconstexpr-steps=-1 -static -g -O3 -o C:\[削除済]\AdventCalender.exe C:\[削除済]\AdventCalender.cpp
C:\[削除済]\AdventCalender.cpp:3:1: error: C++ requires a type specifier for all declarations
print(char * s)
^
C:\[削除済]\AdventCalender.cpp:10:36: error: use of undeclared identifier 'endl'; did you mean 'std::endl'?
std::cout << "Hello,World!" << endl;
^~~~
std::endl
C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30133\include\ostream:977:51: note: 'std::endl' declared here
basic_ostream<_Elem, _Traits>& __CLRCALL_OR_CDECL endl(
^
2 errors generated.
文字の色は勝手にnoteが付けたものだ。消し方があれば教えてほしい。
では読んでみよう...と、最初に少し脱線する。
1.21 ビルド方法のおさらい
まずは一番上の一行を見てみよう。
"C:/Program Files/LLVM/bin/clang++.exe" -std=c++14 -Wall -pedantic-errors -fconstexpr-depth=-1 -fconstexpr-steps=-1 -static -g -O3 -o C:\[削除済]\AdventCalender.exe C:\[削除済]\AdventCalender.cpp
これは何かというと、コンパイラの
「C:/Program Files/LLVM/bin/clang++.exe」
をコンソールから実行するためのものだ。
統合開発環境(ex. Visual Studio)を使っていると意識しないことも多いが、ビルドを行うためには本来このようにいくつかのコンパイラを実行してやる必要がある。
Visual Studioでビルドするときにも、よく見るとコンソールに似たような文字列があるはずだ。たぶん。
Clangでビルドするときには、
clang++.exeのパス 後続のもの以外のコンパイラオプション -o 出力するファイル名 ソースファイル名
というようにする。
最後のほうの-oのすぐ後ろに出力するファイル名が、その後ろにソースファイル名が並んでいる。
それ以外のコンパイラオプションは後で説明する。
もしかしたら他の統合開発環境では違う並びだったりするかもしれないが、とにかく「コンパイラのパス」と「コンパイラオプション」が必要だ。
1.22 おすすめのコンパイラオプション
コンパイラオプションというのは、コンパイラ(より正確にはコンパイラ・ドライバ)に渡してやる諸々のオプションである。
これにより、ビルドの中の様々な処理に細かな指定を行うことが可能だ。
私はあまり知らないので、ここらへんを参考にした。
http://www.yosbits.com/opensonar/rest/man/freebsd/man/ja/man1/CC.1.html?l=ja
-g
これがあるとデバッガーが使える。
-std=c++[バージョン]
どのC++のバージョンでコンパイルするか指定。普通は最新のバージョンを指定。
-O[種類]
最適化を行う。(種類が0で行わない)
いろいろな最適化をまとめてオンオフしている。-oとは別。
-Wall
ほぼ全ての警告を出してくれる。
-pedantic-errors
Clangでは、C++の標準規格にそぐわないもの、例えばコンパイラ独自の拡張や単なる誤記をエラーにする。
-static
詳しくは知らないのでなんとも言えないが、.dllが無くても実行できるようになったりする。その分実行ファイルのサイズは大きくなる。
おすすめのコンパイルオプションとかあれば教えてほしい。
1.23 今度こそコンパイルメッセージを読んでみよう
本題に戻ろう。コンパイルメッセージを読むのが目標だった。
C:\[削除済]\AdventCalender.cpp:3:1: error: C++ requires a type specifier for all declarations
print(char * s)
^
C:\[削除済]\AdventCalender.cpp:10:36: error: use of undeclared identifier 'endl'; did you mean 'std::endl'?
std::cout << "Hello,World!" << endl;
^~~~
std::endl
C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30133\include\ostream:977:51: note: 'std::endl' declared here
basic_ostream<_Elem, _Traits>& __CLRCALL_OR_CDECL endl(
^
2 errors generated.
これがコンパイルメッセージだ。コンパイルメッセージは使うコンパイラによって異なる。が、大体の構成は同じだ。
ファイル名:行数:何文字目 errorまたはwarningまたはnote: 具体的(?)な説明
該当のコード
^~~~~ この辺がおかしいよ~
こう変更したら?
だいたいこんな感じだ。
では、メッセージをみながらコードを直してみよう。
(以下、使っている語句は規格通りでは無いと思う。見かねた場合はご指摘いただけると嬉しい)
まず一つ目のエラーを見てみよう。
どうやら、AdventCalender.cppの3行目1文字目あたりにエラーが出ているらしい。print関数の戻り値の型が無いことを教えてくれている。
printf関数の戻り値を返してもいいが、printf関数の戻り値を確認は面倒なのでvoid型にする。
~~以下蛇足~~
C++では型の明示を必要とするんだ、C++では。と、わざわざ言っているのには意味がある。実は、C言語では戻り値の型を省略できたりする。
#include <stdio.h>
// C言語なら動く(大抵警告が出る)
print(s)
const char * s;
{
printf("%s\n",s);
}
int main(void)
{
print("Hello,ancient C lang!");
// main関数の戻り値の省略は、実はC++でもできる。警告さえも出ない。
}
~~蛇足ここまで~~
次はAdventCalender.cppの10行目36文字目にエラーだ。endlという未宣言の識別子が使われているよ、とのことだ。ご丁寧にstd::endlじゃないかと提案してくれている。その通りなので、そのように直そう。
3つ目のnote、これは何かというと直近のエラーもしくは警告に付随した情報を教えてくれているものだ。
今回の場合はAdventCalender.cppの10行目36文字目にエラーに関する情報、std::endlってのはここで宣言されているよ~と教えてくれている。
さて、以下はこれまでの修正を施したコードだ。
#include <iostream>
print(char * s)
{
printf("%s",s);
}
int main()
{
std::cout << "Hello,World!" << endl;
print("Make a lot of error!");
}
今度はうまくビルドできるだろうか?
C:\[削除済]\AdventCalender.cpp:11:11: error: ISO C++11 does not allow conversion from string literal to 'char *' [-Werror,-Wwritable-strings]
print("Make a lot of error!");
^
1 error generated.
残念ながらまたエラーが出てしまった。
これは -pedantic-errors を付けていない場合、おそらくはC++の11以降でビルドすると警告として表示される。
リテラル文字列から char * への変換は許されていない、とのことだ。おとなしく引数の型を const char * に変更しよう。
さあ、今度こそどうか。
#include <iostream>
void print(const char * s)
{
printf("%s",s);
}
int main()
{
std::cout << "Hello,World!" << std::endl;
print("Make a lot of error!");
}
C:/Program Files/LLVM/bin/clang++.exe -std=c++20 -Wall -pedantic-errors -fconstexpr-depth=-1 -fconstexpr-steps=-1 -static -g -O3 -o C:\[削除済]\AdventCalender.exe C:\[削除済]\AdventCalender.cpp
(base) PS C:\[削除済]>
特に何の出力もない。ビルドに成功したようだ。
(base) PS C:\[削除済]> AdventCalender.exe
Hello,World!
Make a lot of error!
(base) PS C:\[削除済]>
問題なく実行できている。
1.3 メッセージを見てもわからないときは
(0. 翻訳する)
1. エラーメッセージのうち、コード情報を含まない部分を切り出してコピペ
->ググる。
運が良ければ日本語の記事が、普通はStackOverflowの関連する質問がヒットする。
そこの解説を読み、今自分が直面している問題や、それに関連する語句、語句の英語表記などをできる限り把握する。
2. 新たに得た情報を再度ググる。
1と2を繰り返す。それでもわからない、もう自力では新たな情報を見つけられそうにない...というときは、
3. 今までの工程で5分経過してない場合は5分は粘る。
4. 誰かに質問する。
私はだいたい上記のようにしている。
1.4 他の人に質問するときには
・エラーないし警告が出ている場合は、該当箇所のコードとコンソールに出てきたメッセージ全てを見せる。
今見てきたように、メッセージはとても強力な判断材料なのだ。
~~以下蛇足~~
・プログラミングの課題を聞かない / 聞く場合は課題であることを明言する。
知らないうちに課題を教えていることに対し不快感を覚える人は多い。
課題の解説を受けることそのものが悪いのか否かについて、私はこの場で主張するつもりはない。ただ、課題の解説はしたくないという人もいることは念頭に置くべきだ。
・回答者が解説を話したがっているようなら、できるだけ頑張ってきこう。理解できるか否かではなく、理解しようとすることが大切だ。
・回答者の解説(長文)に対し「コードだけでいいです」って言わないで
そもそもQAコミュニティで回答する人々はみな、プログラミングが大好きなんだ。
ほんのちょっとでいいから、頼むから少し話を聞いてみてくれないだろうか。
「ありがとうございます!コードだけでいいです!」はとても辛いんだ...。
なるべく短くするからさ、お願いだから...。
~~蛇足ここまで~~
2章をしっかり書いている余裕がなかった。ほぼ下書きのままだ。
2. コンパイル時計算は意外と簡単?
コンパイル時計算は意外と簡単だ。
…こう書くと語弊がある。普通のプログラミング同様、やりたいことによりけりだ。
しかし、「コンパイル時計算」というワードに拒絶反応をしなくてもいいと思う。やつらは結構便利なのだ。
2.1 そもそもコンパイル時計算ってなに?
2.11そもそもそもコンパイルってなに?
C++コードが機械語になるまでを簡潔に説明する 予定だったが思ってたより難しかった。1章頑張って書いたし説明せんでもいっか...。
ものすごい雑に説明する。
コンパイル時計算とは、実行時ではなくコンパイル時に行われる計算のことだ。
実行時というのは、例えばexeファイルをクリックしてゲームが動いている時のことだ。
対して、コンパイル時というのはエディタのトンカチマークを押してしばらく待っている間のことだ。
こんな認識のやつでもコンパイル時計算はできる。できてしまう。
コンパイル時計算ができる言語
2.12 コンパイル時計算のメリット、デメリット
コンパイル時に計算を行い、いくつかの値を実行前に決定できると様々なメリットがある。
・プログラムが軽くなる
事前に決定できる値を計算しておけば、あとはその値を読みだすだけでいい。なんなら読み出す必要もなく、即値として機械語のコードに書きこんでも問題ないことさえある。
・定数式しか入れられない部分に定数を入れられる。
Nの値をコンパイル時計算すると、
int arr[N];
みたいに書ける。
・書いてて楽しい。
楽しい。
2.13 コンパイル時計算とC++の発展
C/C++には規格がある。
CはK&Rに始まり、現在C23が策定されようとしている。
C++はC with Classesに始まり、C++23が策定されようとしている。
→どんどん便利になっている。
特にコンパイル時計算の機能が強化されてきている。
2.2 レッツコンパイル時計算!
事の発端
「C++とか書きにくいだけ、C#のが書きやすい。
コンパイラが最適化するから、C#もC++もそんな変わんないよ」
よろしい、ならば戦争だ。
0からN-1までの自然数の平方根をどれくらい早く表示できるか競おう
じゃないか。
我々には、そう。コンパイル時計算がある...!
// CompileTimeString.hpp
#pragma once
// C++14想定
#include <cstdint>
#include <type_traits>
// std::bit_cast(C++20)相当のものを使いたいなら(toString(const float)で必要)なおMSVC。
#include <xutility>
#include "Enumerate.hpp"
namespace StewStringCpp14
{
template<size_t N>
struct String
{
char buffer[N]{};
static constexpr const size_t capacity = N;
constexpr String() noexcept = default;
constexpr String(const String&) noexcept = default;
constexpr String& operator=(const String&) & noexcept = default;
~String() noexcept = default;
private:
template<size_t ... Numbers>
constexpr String(const char *const str,StewEnumerate::Enumerate<Numbers ...>) noexcept:
buffer{str[Numbers]...}
{}
public:
constexpr String(const char c) noexcept:
buffer{c}
{}
constexpr String(const char *const str) noexcept:
String(str,typename StewEnumerate::EnumerateMake<N>::type())
{}
template<size_t N2>
constexpr String(const String<N2>& obj) noexcept:
String(obj.buffer,typename StewEnumerate::EnumerateMake<(N2 > N)? N: N2>::type())
{}
constexpr String& operator=(char *const str) & noexcept
{
char *const this_buffer = buffer;
size_t i = 0;
for(;str[i];++i)
{
this_buffer[i] = str[i];
}
this_buffer[i] = '\0';
return *this;
}
constexpr String& operator=(const char c) & noexcept
{
buffer[0] = c;
buffer[1] = '\0';
return *this;
}
constexpr const char& operator[](const size_t i) const& noexcept
{
return buffer[i];
}
constexpr char operator[](const size_t i) const&& noexcept
{
return buffer[i];
}
constexpr char& operator[](const size_t i) & noexcept
{
return buffer[i];
}
constexpr operator const char *() const noexcept
{
return buffer;
}
constexpr size_t size() const noexcept
{
size_t i = 0;
const char *const this_buffer = buffer;
for(;this_buffer[i];++i);
return i;
}
constexpr size_t length() const noexcept
{
size_t i = 0;
const char *const this_buffer = buffer;
for(;this_buffer[i];++i);
return i? i-1 : 0;
}
constexpr String reverse() const noexcept
{
const char *const this_buffer = buffer;
const size_t this_length = length();
if(! this_length) return *this;
String tmp;
for(size_t i = 0;i <= this_length;++i)
{
tmp[this_length - i] = this_buffer[i];
}
return tmp;
}
constexpr String& operator+=(const char *const str) noexcept
{
const size_t this_size = size();
char *const this_buffer = buffer;
size_t i = 0;
for(;str[i];++i) this_buffer[this_size + i] = str[i];
this_buffer[this_size + i] = '\0';
return *this;
}
constexpr String& operator+=(const char c) noexcept
{
const size_t this_size = size();
buffer[this_size] = c;
buffer[this_size + 1] = '\0';
return *this;
}
};
template<size_t N>
constexpr String<N> toString(const char (&str)[N]) noexcept
{
return String<N>(str);
}
inline constexpr String<2> toString(const char c) noexcept
{
return String<2>(c);
}
template<uint8_t MantissaLength = 23>
constexpr String<9 + MantissaLength> toString(const float x) noexcept
{
if(!x)
{
if(std::_Bit_cast<uint32_t>(x)>>31) return toString("-0.0");
else return toString("+0.0");
}
uint32_t x_i32 = std::_Bit_cast<uint32_t>(x);
int16_t exponent = ((x_i32 >> 23) & 0xFF);
const uint32_t mantissa = (x_i32 & 0x7F'FF'FF);
String<9 + MantissaLength> ret_head;
if(x < 0) ret_head += '-';
if(exponent == 0x00)
{
ret_head += "0.";
}
else if(exponent == 0xFF)
{
ret_head += (mantissa == 0)? "inf": "NaN";
return ret_head;
}
else
{
ret_head += "1.";
}
exponent -= 127;
String<6> ret_tail = 'e';
if(!exponent)
{
ret_tail += '0';
}
else
{
if(exponent < 0)
{
ret_tail += '-';
exponent *= -1;
}
String<4> tmp;
for(uint8_t i = 0;exponent;++i) // exponentは破壊的操作される
{
tmp += (exponent % 10) + 48;
exponent /= 10;
}
ret_tail += tmp.reverse();
}
int16_t true_mantissa_size = MantissaLength + 9 - (ret_head.length() + ret_tail.length());
if(true_mantissa_size > 23) true_mantissa_size = 23;
String<MantissaLength + 9> mantissa_str;
for(int8_t i = true_mantissa_size - 1;i >= 0;--i)
{
mantissa_str += ((mantissa >> i) & 0x1) + 48;
}
return ret_head + mantissa_str + ret_tail;
}
// 使い勝手が非常に悪い
template<size_t Lsize,size_t Rsize>
constexpr String<Lsize + Rsize - 1> operator+(const String<Lsize>& l,const String<Rsize>& r) noexcept
{
String<Lsize + Rsize - 1> ret = l;
ret += r;
return ret;
}
}
// Enumerate.hpp
// C++11以降想定
#pragma once
namespace StewEnumerate
{
template<size_t ... Numbers>
struct Enumerate{};
namespace Detail
{
template<size_t N,bool IsN,size_t ... Numbers>
struct EnumerateHelper
{
using type = Enumerate<Numbers ...>;
};
template<size_t N,size_t ... Numbers>
struct EnumerateHelper<N,false,Numbers ...>
{
using type = typename EnumerateHelper<N,sizeof...(Numbers) == N - 1,Numbers ...,sizeof...(Numbers)>::type;
};
}
template<size_t N>
struct EnumerateMake
{
using type = typename Detail::EnumerateHelper<N,0 == N - 1,0>::type;
};
}
#include <cstdint>
#include "CompileTimeString.hpp"
#include <xutility>
#include <iostream>
constexpr float mySqrt(const float number) noexcept
{
constexpr float three_halfs = 1.5;
const float x2 = 0.5F * number;
float y = number;
uint32_t x_int32 = std::_Bit_cast<uint32_t>(number);
x_int32 = 0x5f3759df - (x_int32 >> 1);
y = std::_Bit_cast<float>(x_int32);
y = y * (three_halfs - x2 * y * y);
y = y * (three_halfs - x2 * y * y);
return y * number;
}
template<int N>
constexpr auto makeSqrtFloatStrings() noexcept
{
using namespace StewStringCpp14;
String<33 * N + 1> ret = toString("");
for(int i = 0;i < N;++i)
{
ret += toString(mySqrt(i)) + toString('\n');
}
return ret;
}
int main()
{
using namespace StewStringCpp14;
constexpr auto str = makeSqrtFloatStrings<10000>();
std::cout << (const char *)str << std::endl;
std::cout << std::endl;
int hoge;
std::cin >> hoge;
return 0;
}
コンパイルは約1時間くらい。多分劇的に短くすることもできなくはないのだが、よりによって一番頑張った部分に変更を加えなければならないのでやめた。
2.3 実行の様子
動画の録画ができませんでした。
動いたんだ、信じてほしい。
とても速い!!!
exeファイルをクリックすると同時に出力済みの画面が出てきた。
(信頼性の薄い供述)
ちなみにC#は表示までに2秒ちょいかかったらしいよ。
2.4 コードの説明
2.41 constexprで高速な平方根の計算
constexpr関数にすればいいだけだ。実装はhttps://takashiijiri.com/study/miscs/fastsqrt.html
をコピペしてキャストまわりを安全にした。
constexpr float mySqrt(const float number) noexcept
{
constexpr float three_halfs = 1.5;
const float x2 = 0.5F * number;
float y = number;
uint32_t x_int32 = std::_Bit_cast<uint32_t>(number);
x_int32 = 0x5f3759df - (x_int32 >> 1);
y = std::_Bit_cast<float>(x_int32);
y = y * (three_halfs - x2 * y * y);
y = y * (three_halfs - x2 * y * y);
return y * number;
}
2.42 contexprで(めっちゃ鈍重な)文字列操作
さて、constexprに平方根を計算したし、あとは表示するだけだ!
...ともいかない。標準出力に計算した数値を出すのは、実はそこそこの処理を行っているはずだ。これは、実行時前に可能だ。
ここもコンパイル時にこなしてしまおうじゃないか。
C++の文字列操作といえば、std::strngである。
だが、これはconstexpr関数内ではまだ使用できない。GCCやClangではまだ不可能だ。
そこで、コンパイル時に使える文字列クラスを自作することとなった。
最低限必要な機能さえあれば良いのであまり使い勝手は気にしない。
とりあえず、
目標1. デフォルト / コピーコンストラクタがある
目標2. char型や文字列リテラル、char * 型を受け取るコンストラクタがある
目標3. +、+=がある
これだけあれば今回は十分だろう。
結論から言うと、あんまりうまくいかなかった...。
特に、目標2は達成できなかった。
それから、コンパイルがとても遅くなってしまった。
まず、クラスがどのようにして文字列を保持するかを考える。
コンパイル時のメモリ動的確保は...今調べなおしたらできそうだな...
コードを書き始めた時点では知らなかった&そもそも難しそうなのでメモリ動的確保はしないことにした。
つまり、静的に記憶域をとることにした。つまるとこただの配列(生配列)を使った。
こうなると、ある程度古いバージョンでも使える。どうせなら古くても使えるようにすることにした。最終的にC++14で動くことが確認できた。
#include <cstdint>
template<size_t N>
struct String
{
char buffer[N]{};
static constexpr size_t capacity = N;
};
N、capacityはこのオブジェクトに格納される文字列のヌル終端を含めた文字数である。
これにデフォルトコンストラクタとコピーコンストラクタ、ついでにコピー代入演算子とデストラクタを用意する。
template<size_t N>
struct String
{
char buffer[N]{};
static constexpr size_t capacity = N;
constexpr String() noexcept = default;
constexpr String(const String&) noexcept = default;
~String() noexcept = default;
};
さて、目標2に移ろう。
文字を受け取るコンストラクタは簡単だ。
constexpr String(const char c) noexcept
buffer{c}
{}
これでいい。
問題は、文字列リテラルやchar配列と、const char * を受け取る場合だ。
constexpr なユーザー定義クラスは、constexprコンストラクタを持っている必要がある。
このconstexprコンストラクタ、いろいろな制約があるのだが、わかりやすく大変なのは「本体が空でなければならない」ということだ。
つまり、
// NG
constexpr String(const char *const str) noexcept
{
for(size_t i = 0;str[i];++i) buffer[i] = str[i];
}
というようなことはできない。
メンバ初期化子でなんとかして初期化してやる必要がある。
ここで「可変長テンプレートパラメーターパック」というものの利用を閃いた。我ながら天才では?と思ったが、後々調べたらずいぶん前から広く知られていたアイデアであったようだ。
template<size_t ... Numbers> // なんとかしてNumbersを推論する
constexpr String(const char *const str) noexcept
buffer{str[Number]...}
{}
まさかNumbersを自分で0,1,2,3,...と指定するわけにもいくまい。
さて、どうやって可変長引数テンプレートパラメーターパックNumbersを推論したものか。
結論から言うと、引数からの推論以外知らなかった。で、デフォルト引数からはテンプレートを推論してくれないらしい。そこで実引数を与えてやることにした。
Numbersを包んだEnumerateというクラステンプレートを用意し、Enumerate経由でNumbersを推論する。
template<size_t ... Numbers>
constexpr String(const char *const str,Enumerate<Numbers>) noexcept
buffer{str[Number]...}
{}
呼び出し側ではEnumerate<0,1,2,3>といったようにいちいち0からN-1までの引数を与えるのでは引数推論の意味がないので、EnumerateMake<N>::type がEnumerate<0,1,2,…,N-1> となるようにEnumerateMakeというクラステンプレートを用意する。
// Enumerate.hpp
// C++11以降想定
#pragma once
namespace StewEnumerate
{
template<size_t ... Numbers>
struct Enumerate{};
namespace Detail
{
template<size_t N,bool IsN,size_t ... Numbers>
struct EnumerateHelper
{
using type = Enumerate<Numbers ...>;
};
template<size_t N,size_t ... Numbers>
struct EnumerateHelper<N,false,Numbers ...>
{
using type = typename EnumerateHelper<N,sizeof...(Numbers) == N - 1,Numbers ...,sizeof...(Numbers)>::type;
};
}
template<size_t N>
struct EnumerateMake
{
using type = typename Detail::EnumerateHelper<N,0 == N - 1,0>::type;
};
}
呼び出し側は以下のようにする。
constexpr const char *const p = "Hello,CompileTimeString!";
constexpr const size_t n = sizeof("Hello,CompileTimeString!");
String<n> str(p,EnumerateMake<n>);
EnumerateMakeをStringを使うたびに呼び出すのはかっこ悪い。
また、リテラル文字列、つまりchar配列に変換できるものまで要素数の情報を失うconst char *const で受け取って、その文字列の長さぴったりをテンプレート引数に渡してやらなければならないのも使い勝手が悪い。
そこで、他の関数のような何かからこのコンストラクタを呼び出してやるようにする。
まずは委譲コンストラクタを使ってどうにかできないか考える。
private:
template<size_t ... Numbers>
constexpr String(const char *const str,StewEnumerate::Enumerate<Numbers ...>) noexcept:
buffer{str[Numbers]...}
{}
public:
constexpr String(const char *const str) noexcept:
String(str,typename StewEnumerate::EnumerateMake<N>::type())
{}
constexpr String(const char (&str)[N]) noexcept:
String(str,typename StewEnumerate::EnumerateMake<N>::type())
{}
template<size_t N2>
constexpr String(const String<N2>& obj) noexcept:
String(obj.buffer,typename StewEnumerate::EnumerateMake<(N2 > N)? N: N2>::type())
{}
委譲コンストラクタを使うとこういうこともできる。
一番下のコンストラクタはString<N2>からString<N>へとコピーするためのものだ。
ちなみに、クラステンプレートのテンプレート引数の推論をコンストラクタから行うのはC++17からの機能だ。
で、これにて一件落着かというと...実はそうでもない。
このStringコンストラクタにリテラル文字列を渡してやると、
String(const char *const)とString(const char (&)[N])のどちらを呼び出せばいいのか曖昧だ、とエラーを吐かれる。
個人的に、const char *const と const char (&)[N] なら後者のほうが優先されてしかるべきだと思う。もしくはリテラル文字列をどんな型よりも優先して受け取り、その要素数を定数式でも使えるような機能があってもいいんじゃないかと思う。
リテラル文字列は融通が利かないので、仕方なくリテラル文字列はtoStirng関数を使ってStringオブジェクトにする、ということにした。
関数の引数からテンプレート引数を推論するのはC++14でもできる。
template<size_t N>
constexpr String<N> toString(const char (&str)[N]) noexcept
{
return String<N>(str);
}
inline constexpr String<2> toString(const char c) noexcept
{
return String<2>(c);
}
charを受け取る場合はデフォルトでサイズを2にしたいので、
String<2> toString(const char)も作った。
あとは適当に欲しい機能をメンバ関数として加える。
constexpr String& operator=(char *const str) & noexcept
{
char *const this_buffer = buffer;
size_t i = 0;
for(;str[i];++i)
{
this_buffer[i] = str[i];
}
this_buffer[i] = '\0';
return *this;
}
constexpr String& operator=(const char c) & noexcept
{
buffer[0] = c;
buffer[1] = '\0';
return *this;
}
constexpr const char& operator[](const size_t i) const& noexcept
{
return buffer[i];
}
constexpr char operator[](const size_t i) const&& noexcept
{
return buffer[i];
}
constexpr char& operator[](const size_t i) & noexcept
{
return buffer[i];
}
constexpr operator const char *() const noexcept
{
return buffer;
}
constexpr size_t size() const noexcept
{
size_t i = 0;
const char *const this_buffer = buffer;
for(;this_buffer[i];++i);
return i;
}
constexpr size_t length() const noexcept
{
size_t i = 0;
const char *const this_buffer = buffer;
for(;this_buffer[i];++i);
return i? i-1 : 0;
}
constexpr String reverse() const noexcept
{
const char *const this_buffer = buffer;
const size_t this_length = length();
if(! this_length) return *this;
String tmp;
for(size_t i = 0;i <= this_length;++i)
{
tmp[this_length - i] = this_buffer[i];
}
return tmp;
}
constexpr String& operator+=(const char *const str) noexcept
{
const size_t this_size = size();
char *const this_buffer = buffer;
size_t i = 0;
for(;str[i];++i) this_buffer[this_size + i] = str[i];
this_buffer[this_size + i] = '\0';
return *this;
}
constexpr String& operator+=(const char c) noexcept
{
const size_t this_size = size();
buffer[this_size] = c;
buffer[this_size + 1] = '\0';
return *this;
}
これでchar、文字列リテラル、char * からStringに変換したり代入したりできるようになった。
しかしStringコンストラクタは現状floatを受け取ることができない。ので、floatからStirngをつくるようなtoString(const float)関数を書く。
std::sprintfなどもコンパイル時には使えないので大変だ。
template<uint8_t MantissaLength = 23>
constexpr String<9 + MantissaLength> toString(const float x) noexcept
{
if(!x)
{
if(std::_Bit_cast<uint32_t>(x)>>31) return toString("-0.0");
else return toString("+0.0");
}
uint32_t x_i32 = std::_Bit_cast<uint32_t>(x);
int16_t exponent = ((x_i32 >> 23) & 0xFF);
const uint32_t mantissa = (x_i32 & 0x7F'FF'FF);
String<9 + MantissaLength> ret_head;
if(x < 0) ret_head += '-';
if(exponent == 0x00)
{
ret_head += "0.";
}
else if(exponent == 0xFF)
{
ret_head += (mantissa == 0)? "inf": "NaN";
return ret_head;
}
else
{
ret_head += "1.";
}
exponent -= 127;
String<6> ret_tail = 'e';
if(!exponent)
{
ret_tail += '0';
}
else
{
if(exponent < 0)
{
ret_tail += '-';
exponent *= -1;
}
String<4> tmp;
for(uint8_t i = 0;exponent;++i) // exponentは破壊的操作される
{
tmp += (exponent % 10) + 48;
exponent /= 10;
}
ret_tail += tmp.reverse();
}
int16_t true_mantissa_size = MantissaLength + 9 - (ret_head.length() + ret_tail.length());
if(true_mantissa_size > 23) true_mantissa_size = 23;
String<MantissaLength + 9> mantissa_str;
for(int8_t i = true_mantissa_size - 1;i >= 0;--i)
{
mantissa_str += ((mantissa >> i) & 0x1) + 48;
}
return ret_head + mantissa_str + ret_tail;
}
10進数で表示するのはつらかった。
そこで仮数部は二進数のままにすることにした(妥協)
一応正規化数以外にも対応したつもりだ。
floatのビットを読むためにキャストをしている。*(uint32_t *)&xとかすると未定義動作なのでMSVCの_Bit_castを使っている。これはC++20ではstd::bit_castとして使えるようになっている。
指数部、仮数部を読み取って文字列に変換している。
ともかく、これでfloatをStringに変換できる。
あとは平方根を計算してStringにし結合すればいい。で、表示する。
template<int N>
constexpr auto makeSqrtFloatStrings() noexcept
{
using namespace StewStringCpp14;
String<33 * N + 1> ret = toString("");
for(int i = 0;i < N;++i)
{
ret += toString(mySqrt(i)) + toString('\n');
}
return ret;
}
int main()
{
using namespace StewStringCpp14;
constexpr auto str = makeSqrtFloatStrings<10000>();
std::cout << (const char *)str << std::endl;
std::cout << std::endl;
int hoge;
std::cin >> hoge;
return 0;
}
これで完成だ。
多分Enumerate使ってる部分をなくせば、コピーはできないがはるかにコンパイルは早くなるんじゃないかな。
2.5 愚痴
リテラル文字列、もっと扱いやすくなってほしい。
配列の初期化も楽にしてほしい。
2.6 おまけ
Visual Studio(正確にはMSVC)ではconstexpr std::stringが使えるらしい。
つまり、上記のようなコードを書く必要はないようだ。
私は何を必死になって書いていたんだろう...
…と、これをオチにするつもりだったのだが、MSVCを使ったところでどうにもうまくいかなかった。
C++のバージョンの指定がうまくいかなかったのかな?
ともかく、このコードにもまだ使い道があるかもしれない。やったね!()
この記事が気に入ったらサポートをしてみませんか?