C++プログラミング覚え書き

鷺澤伸介
(初稿 2021.12.25)
(最終改訂 2023.8.29)

constの位置について

constの「機能」はともかくとして、その「書法」について詳細に説明してくれている書物やサイトは、思いのほか少ないようです。constを書くときはいつももやもやしていたので、いろいろと調べてみました。調べた過程をそのまま、あまり整理せずに書いていますので、結論だけをお読みになる場合は、下の「解決編」をご覧ください。
実用的な判断方法
調査編:ISO Standard C++
調査編:プログラミング言語C++第4版
調査編:英語Wikipedia
調査編:プログラム言語C++(2003)
解決編:C++11の文法と機能

実用的な判断方法

調べたことを書く前に、普段採用している読み方を書いておきたいと思います。実用的には、constがある場合は次のように判断すればほぼ間違いないように思います(constメンバ関数は除く)。
constの読み方
・constは「型」か「*」を修飾していると考える。
・constが「型」を修飾しているなら値不変、「*」を修飾しているならポインタ不変である。
・右から左に読んでいき、constがあったらその「左」直近の語を修飾していると見る。
・ただし、左に「型」も「*」もない場合は、その「右」直近の語を修飾していると見る。
この方針で実例を読んでみると、
となります。constが不変にしているのが「型(値)」なのか「ポインタ」なのか、すべて正しく判定できています。見やすくなるように、コードの方にも括弧を付けると、
となります。
なお、「*」は必ず型の右側に来ますので、「右修飾」の場合は「*」を修飾することはありません(「const* int a;」という書き方はあり得ない)。よって、「右修飾の場合の被修飾語は型である(=値不変)」と考えてもかまわないでしょう。
ダブルポインタやトリプルポインタの間にconstが割り込んでいる(例:* const*)ような分かりにくいケースであっても、とにかく右から左へ読んで、「constは左直近の型かポインタを修飾」「左に型もポインタもない場合は右直近の型を修飾」と捉えていけば、おそらく大丈夫だと思われます。
★「** const」「*** const」などでは、constが修飾するのは隣接している最後の「*」だけです。トリプルポインタすべてを修飾させたいのなら「* const* const* const」と書く必要があります。
ただ、実用的にはこれでほぼ困らないとはいえ、const配置位置は規格的にはどうなっているのでしょうかなぜそれにこだわるのかというと、いざ自分で書くときに「const int」と書くべきなのか「int const」と書くべきなのか、決め手というか、理論武装のようなものが欲しいからです。「const int」と「int const」は、本当はどちらが正しいのか。どちらも正しいというのなら、いったいどういう規格になっているのか。以下はその調査過程と結果です。

調査編:ISO Standard C++

C++の始祖ストラウストラップさんが役員に名を連ねる「ISO Standard C++」サイトのFAQ、「Const Correctness(Constの正確性)」ページでは、一貫して「右から左へ読むべし」「constはすべて被修飾語の右に置く方がよい」という助言を与えています。以下、この中の1項目を訳してみます。
「X const& x」と「X const* x」はどういう意味ですか?
「X const& x」は「const X& x」と同等、「X const* x」は「const X* x」と同等です。
const右配置スタイルを好む人々がいて、それを「consistent const(一貫性のあるconst)」と呼んだり、サイモン・ブランドによる造語を使って「East const(東のconst)」と呼んだりしています。実際、「East const」スタイルは、ほかの選択肢よりも一貫性があります。「East const」スタイルでは、constはconst化するものの右側に常に置かれるのに対し、ほかのスタイルでは、constは左側に置かれたり右側に置かれたりするのです(右側に置かれるのはconstポインタ宣言やconstメンバ関数の場合です)。
「East const」スタイルでは、constであるローカル変数は、int const a = 42;のように右側のconstで定義されます。同様に、constである静的変数は、static double const x = 3.14;のように定義されます。基本的に、すべてのconstは、それがconst化するものの右側に置かれることになり、constポインタ宣言やconstメンバ関数のように右側に置くことが要求されるconstも含んでいます。
また、「East const」スタイルは、型のエイリアスと一緒に使っても混乱しにくいのです。ここで、fooとbarとではなぜ型が違うのでしょうか?
using X_ptr = X*;
const X_ptr foo;
const X* bar;
★一見同じように見えますが、上のconstは「ポインタ修飾」で、下のconstは「型修飾」になります(typedefを使っても同じ)。エイリアスは、単純にテキスト置換をするだけのマクロと違って、「X*」でひとまとまりとして扱っているように見えます。ですから、上のconstは「X*」の「*」まで修飾するのに対し、下のconstは「X」しか修飾しません。このように、直近の被修飾語のさらに先のそれまでconstに修飾させるような方法は、エイリアスでしか実現できそうにありません(「X*」をひとまとめにしたいからといって、(X*)などと括弧でくくるわけにはいかないので)。エイリアスの使い方でよく紹介されている、「using int* int_p; int_p a, b, c, d;」という場合も、「int* a, b, c, d;」ではなく「int* a, *b, *c, *d;」と書くのと同じになるということで、ここでのエイリアス「int_p」は「int*」をひとまとまりとして扱っていることが分かります。
「East const」スタイルを使えば、これがより明確になります。
using X_ptr = X*;
X_ptr const foo;
X* const foobar;
X const* bar;
ここでは、fooとfoobarは同じ型であり、barは違う型であることがより明確になっています。
「East const」スタイルは、ポインタ宣言とより一貫性があります。従来のスタイル
const X** foo;
const X* const* bar;
const X* const* const baz;
と、「East const」スタイルとは、対照的です。
X const** foo;
X const* const* bar;
X const* const* const baz;
★ポインタ宣言では、従来のスタイルだと右修飾と左修飾が混在してしまうが、「East const」スタイルならすべて左修飾で統一できる、と言いたいようです。
このような利点があるにもかかわらず、const右配置スタイルはまだ一般的ではないため、レガシー・コードでは従来のスタイルを採用する傾向があります。
なるほど、constを右配置するメリットはよ~く分かりました。今後はすべて「const右配置」で統一しようと思います。
しかし、「const配置位置の規格」についての説明は、このページにはありません。もっと調べてみる必要があります。

調査編:プログラミング言語C++第4版

ストラウストラップさん繋がりで、彼の大著『プログラミング言語C++第4版』(柴田望洋・訳、SBクリエイティブ、2015)を繙いてみると、「7.5 ポインタとconst」に
プログラミング言語C++第4版
・値を不変にするにはconstを接頭語にする。例:const int model = 90;
・ポインタを不変にするには宣言演算子「*const」を用いる。例:char *const cp = s;
・「const*」という宣言演算子は存在せず、この場合のconstは型の一部と見なされる。例:char const* pc;
という内容が書かれていました。「*const」を、これで一つの宣言演算子として扱っており、この形の場合はポインタ不変で、それ以外の場合のconstは型の一部(つまり値不変)になる、とのこと。なるほど、シンプルで明快です。
ここでのストラウストラップさんは、上の「ISO Standard C++」と違って、型修飾の場合のconstを接頭語とする(=左配置)伝統的な書法を採用しています。それでも、右から左へ読むとよいことにも触れていて、「このような宣言を右から左へ読むと分かりやすいことを発見した人々がいる。たとえば、(char *const cp;なら)“cpはconstなポインタであり、その指す先はcharである”、(const char* pc2なら)“pc2はポインタであり、その指す先はchar constである”となる」とお書きになっています。
この説明だけでもconstの読み方には困らないと思います。しかし、この節にも、「6.3 宣言」の節にも、「const配置位置の規格」についての明確な説明はありません。

調査編:英語Wikipedia

英語Wikipediaの「const (computer programming)」のページ(2021年12月15日現在)では、「Syntax(構文)」の項に書法の説明があります(以下の具体例は、すべて初期化省略)。
まず「単純なデータ型」のconstについては、「ポインタのないシンプルなデータ型にconst修飾子を適用するのは簡単です。歴史的な理由から、型のどちらの側にも置くことができます(例えば、const char foo = 'a';は、char const foo = 'a';と同じです)」となっています。説明はこれだけで、型のどちら側に書いてもよい理由については「for historical reasons(歴史的な理由から)」としか書かれていません。
次に「ポインタと参照」のconstについては、「意味はより複雑です」「構文も混乱しかねません」と注意書きがあり、その後、CとC++間での慣例の違いと、それぞれのconstの読み方が説明されています。その説明によると、Cのconstは「右の変数名を修飾する」、C++のconstは「左の型を修飾する」ものとして読むように、とのこと。
★上のCの「*ptr」は、宣言ですからいずれも間接参照ではありません。これらを間接参照扱いにしているのは、説明のための便宜的な処置でしょう。
★下のC++の例は、当該ページではダブルポインタの例が挙げられています。シングルポインタでも同じことなので、ここではCの例に合わせておきました。それと、当該ページでは「*」の位置がC++であっても基本的に右寄せになっており、曖昧な書き方になっている箇所もあります。ここではCとC++で寄せる方向を変え、それぞれで統一しておきました。
要するに、Cではconstの右側の「変数名」が「*ptr」(値)なのか「ptr」(ポインタ)なのかで、C++ではconstの左側の「型」が「int」(値)なのか「int*」(ポインタ)なのかで、値が不変なのかポインタが不変なのかを判断する、ということのようです。ポインタ記号は、Cでは右寄せ、C++では左寄せにするのが慣例なので、その違いに合わせた読み方です。まあ読み方は違っていても、結果はどちらも同じになりますから、分かりやすい方で読めばいいと思います。ただ、いちばん下の例のようにconstが複数出てくるケースでは、どちらの読み方で読んでもなかなか分かりにくいですよね。
C++でのポインタ絡みconstの読み方説明の途中で、constを型の左側に置いてもよいケース、すなわち頭にconstを書いてよいケースが、もう一度出てきます。「場合によっては、C/C++ではconstキーワードを型の左側に置くことが許されます」として、その例が下記のように書かれているのです(一部改)。
このようなケースでなぜconstを左に置いてもよいのかの説明は、ここにも書かれていません。単に「In some cases(場合によっては)」と書いてあるだけです(上では「歴史的な理由から」と書かれていました)。「場合によっては可能」だというのなら、その「場合」とはどういう「場合」なのかを書いておいてほしいものです。
ただし、このような「constの前置」は混乱の元である、というようなことも書かれていますので、許されるからといって書くことを推奨するものではない、というのがこのページのスタンスのようです。このページを書いた人は、おそらく「const右側配置 = consistent constスタイル、East constスタイル」な人なのでしょう。
以下、このページで分かったことをまとめておきます。
英語Wikipedia「const (computer programming)」
・constは、Cでは「右の変数名」を、C++では「左の型」を修飾する。(結果は同じ)
・ポインタが含まれる場合は、以下のように読む。
……Cでは、「*ptr」修飾なら値不変、「ptr」修飾ならポインタ不変である。(ptrは変数名)
……C++では、「int」修飾なら値不変、「int*」修飾ならポインタ不変である。(intは型名)
・C/C++とも、constを型名の前に置けるケースがあるが、使用は推奨されない。

【追記1】

このページでは、この後に「ポインタ記号は常にできるだけ右側に書き込んでください」という、ポインタ変数宣言時の推奨事項が出てきます。
ポインタ記号を左寄せにしてしまうと、宣言1文中で変数を複数並べる場合に紛らわしくなったり醜くなったりするから、というのがその理由のようです。この「ポインタ記号を左寄せにすべきか右寄せにすべきか問題」は、これまでかなり頻繁に考察されたり議論されたりしてきたテーマで、ネット上では実にさまざまな意見を見ることができます。
★柴田望洋さんは、『新・明快C++入門』で、左寄せスタイルを採用しつつ、次のような注釈をお書きになっています。「C言語のプログラムでは、ポインタを宣言する際の*は、intの後ろに付けるのではなく、int *p = &x;と、変数名の前に付けるスタイルが一般的です(C++のプログラムでも、このスタイルで書いても構いません)。intと*を付けるC++の慣習は、《整数のint》と《~へのポインタの*》という二つの単語であるint*を、あたかも《intへのポインタ》という一つの型名のように使いたい、という願望の表れです」
私としては特にこだわりはありませんので、「Cでは右寄せ」「C++では左寄せ」と、一般に採用されているスタイルをそのまま踏襲しています。ただ、左寄せにした方が「宣言」(int[* a])と「間接参照」([*a])の違いがはっきりするので、上のような問題はあるにせよ、個人的にはC++的な左寄せ記法の方が好みです。変数を1文中に複数並べると紛らわしくなるというのなら、変数を1文につき1個ずつ宣言すればよいだけの話ですしね。

【追記2】

このページでは、小見出しが「Pointers and references(ポインタと参照)」となっている割には、「参照」へのconstの付け方の説明が書かれていません(正確には、書かれていることは書かれているが、上記の右寄せか左寄せか問題の例としてしか出てこない)。考え方としてはポインタと同じでよい、ということなのかもしれませんが、参照では、ポインタなら許される書き方が許されないケースがあります。
上のように、参照の場合は「値不変」の場合にしかconstは付けられません。これは、参照の場合、参照先のアドレスを参照側からは変更できないから(=「アドレス不変」にするためのconstは付けるだけ無駄だから)、ということなのでしょう。基本的なことかもしれないけれど、小見出しに「参照」を入れている以上は、こういうことまできちんと説明してほしかったところです。まあWikipediaなので、今後いくらでも書き足される可能性はあるのですけれども。

調査編:プログラム言語C++(2003)

「行き詰まったときは基本に戻れ」というわけで、少し古い規格ではあるけれど、JIS、日本産業標準調査会の『プログラム言語C++』(2003)を見てみました(正式な規格が知りたいなら最初から規格書に当たれよ、と言われそうですがw)。第7章「宣言」と第8章「宣言子」あたりを見ればよいのでしょうか。私のような文系老人にはとても歯が立たないような難しい内容がたくさん書き連ねられているのを我慢して目を通していくと、103ページの「8. 宣言子」に、
《宣言》は、指定子[《宣言指定子列》(7.1参照)]及び宣言子(《初期化宣言子並び》)から構成される。指定子は、宣言するオブジェクト、関数又は型定義に対して、型、記憶域種別又はその他の特性を指定する。宣言子は、そのオブジェクト、関数又は型定義に対して、名前を指定する。
と書いてあり、また106ページの「8.3 宣言子の意味」には、
したがって、ある識別子の宣言は、次の形式となる。
T D
ここで、Tは宣言指定子列、Dは宣言子とする。
と書いてありました。どうやら、宣言は「指定子」と「宣言子」の二つの部分から構成されている、ということのようです。それでは、「指定子」と「宣言子」とは、それぞれ何なのか。前後を探してみると、
【指定子】──記憶域種別指定子、型指定子、関数指定子、friend、typedef
・記憶域種別指定子──static、extern、mutableなど
・型指定子──char、int、float、double、voidなど
・関数指定子──inline、virtual、explicit
【宣言子】──「名前を指定する」とあるので、名前(識別子)周りの部分だろう。「宣言子は、*(へのポインタ)及び()(を返す関数)などの演算子を含めることで指定子の型を変更することができる」とあるので、ポインタ(*)はこちらに属する。参照(&)、配列([])、関数(())もこちらである。
といったものであることが判明します。以上のことを実際の宣言に当てはめてみると、
となるでしょうか。
で、肝心のconstはどうなのか。constはvolatileとともに頭文字を取って「cv修飾子」と呼ぶそうです。その「cv修飾子」は、指定子と宣言子のどちらのページにも登場します。両方から関係ありそうな箇所を拾ってみます。
指定子のcv修飾子→
《cv修飾子》が《宣言指定子列》にある場合、その宣言の《初期化宣言子並び》は、空であってはならない。(p.87)
cv修飾付き型へのポインタ又は参照は、cv修飾付きオブジェクトを実際に指しているか参照しているかする必要はないが、そうしているとして扱われる。const修飾付きのアクセスパスを使って、オブジェクトを変更してはならない。この禁止は、参照されるオブジェクトが定値オブジェクトであるか否か、他のアクセスパスから変更できるか否かに依らない。(p.87)
……上は、cv修飾子があったら初期化せよ、と言っているようです。下は、const int* aのような場合は、ポインタ先がconst intである必要はなくただのintでもいいが、いずれにしても値不変で変更不可になる、と言っているようです。こちらの場合は、この説明の下に並んでいる例の一つ「const int* cip;」から判断して、必ずしも初期化は必要ないようです。「初期化については分かった。で、constの配置位置はどうすればいいのか?」と思って探してみたけれど、それについての説明は(たぶん)書かれていません。例を見ると、今引いた「const int* cip;」のように前置スタイルになっています。
宣言子のcv修飾子→
宣言“T D”において、Dが、“*cv修飾子列optD1”という形であって、宣言“T D1”の中の識別子の型が“派生宣言子型並びT”である場合、Dの識別子の型は、“派生宣言子型並び 及び《cv修飾子列》の付いたTへのポインタ”とする。《cv修飾子》は、そのポインタに適用されるが、それが指すオブジェクトには適用されない。
参考 派生宣言子型は、複合型(3.9.2)のうち、クラスに関係する型を除く型とする。派生宣言子型並びは、派生宣言子型を指す宣言子の並びとする。(p.106)
……何だか読みにくい文章ですね。「宣言子が『*const D1』となっている場合は、const付きの、Tへのポインタ型となる。この場合のconstはポインタに適用され、それが指す先には適用されない(=ポインタ不変、値は可変)」という理解でよいのでしょうかとすると、さらにかいつまんで、「宣言子では、constは『*const』の形で、ポインタを不変にする働きをする」と考えてよさそうです。
★「派生宣言子型」とは、「配列、参照、ポインタ、関数」などの型を指すようです。
★読みにくい印象を受けたのは、「派生宣言子型並び及び《cv修飾子列》の付いたTへのポインタ」の部分の修飾・被修飾の関係が、一読では分からないからだと思います。「派生宣言子型並び及び《cv修飾子列》の付いたT、へのポインタ」なのか、「派生宣言子型並び及び《cv修飾子列》の付いた、Tへのポインタ」なのか。この規格書では「Tへのポインタ」でひとまとまりの用語であることを知らないと、誤読してしまいそうです。
★「*const」の形は、指定子には現れません。ポインタ記号「*」は宣言子で、それより右側はすべて宣言子になるからです。
★「int* const a;」は、初期化しないとコンパイラが通してくれません。「const int* a;(=int const* a;)」は初期化しなくても通るのに……。値固定の「const int* a;(=int const* a;)」を①、ポインタ固定の「int* const a;」を②とし、ポインタとポインタ先を「a→int_vaue」と図式すると、①はaの先にあるint_valueは固定するけれどもaそのものを固定しているわけではない、すなわち「aは変更可能だから初期化は必要ない」のに対し、②はaそのものを固定している、すなわち「aは変更できないので初期化が必要」、という理屈でしょうか?
★5行目の「int *const x」は、指定子にconstがないので、それがある&zやyでは初期化できません。
以上、分かったことをまとめます。
日本産業標準調査会『プログラム言語C++』(2003)
・宣言は、指定子と宣言子から成る。
・constなどの「cv修飾子」は、指定子にも宣言子にも現れる。
・宣言子に「*const」がある場合は、constはポインタを不変にする。
constが「*const」という形になる場合はポインタ不変である、という内容は、上のストラウストラップさんの著書にも書いてありました。だいぶ分かってきましたが、ここでも「const int」と書いても「int const」と書いてもよい理由は見当たりません。残念ながら肝心なことは分からないままでした。

解決編:C++11の文法と機能

面白くて頼りになる江添亮さんの『入門C++』(GitHub)では、「constはちょっと文法が変わっていて混乱する」「これはとても複雑なルールで決まっているので、こういうものだとあきらめて覚えるしかない」という説明しかありません。しかし、江添さんは、『C++11の文法と機能』(GitHub)の方では詳しい説明をお書きになっています。
うれしいことに、ここにやっと欲しかった情報がありました「7.3 指定子(Specifiers)」に、
指定子には、ストレージクラス指定子、関数指定子、typedef指定子、friend指定子、constexpr指定子、型指定子がある。
指定子は、組み合わせて使うことができる場合もある。例えば、typedef指定子と型指定子は、組み合わせて使うことができる。その際、指定子の順番には、意味が無い。以下の2行のコードは、全く同じ意味である。
// int型の別名typeを宣言
// typedefはtypedef指定子、intは型指定子、typeは宣言子
typedef int type ;
int typedef type ;
もちろん、指定子と宣言子は違うので、以下はエラーである。
// エラー、*は宣言子。宣言子の後に指定子を書く事はできない
int * typedef type ;
という記載があり、その後の「7.3.6.1 CV修飾子(The cv-qualifiers)」にも、
CV修飾子(cv-qualifier)は、指定子の他に、ポインターの宣言子にも使うことができる。CV修飾子には、constとvolatileがある。この二つのキーワードの頭文字をとって、CV修飾子と呼ばれている。CV修飾子付きの変数は、必ず初期化子が必要である。CV修飾子がオブジェクトに与える影響については、基本事項の3.9.3 CV修飾子を参照。
const int a = 0 ;
volatile int b = 0 ;
const volatile int c = 0 ;
const int d ; // エラー、初期化子がない
指定子の始めに述べたように、指定子の順番に意味はないので、const intとint constは、同じ意味となる。
という記載があったのです(強調引用者)。
「指定子の順番に意味はない」のであれば、「const int」でも「int const」でもお好きな方で書いてください、ということになるわけです。そこで、指定子は本当に順不同でいいのかどうか、実験してみました。
なるほど、確かに指定子はどういう順序で並べてもかまわないようです。私にとって、これはかなり意外な事実でした。friend、typedef、static、inlineなどは型より先に書くものだと思い込んでいて、実は型の後ろに持ってきてもよいなどとは思いもしませんでしたから。そもそも、staticやinlineを指定子列の最後に書いているコードなど見たことがなかったですしね。
というわけで、これでやっと指定子列のconstが前置だったり後置だったりする理由がはっきりしました。ただ単に「記述順序が決まっていないから」だったのですね。正直、「な~んだ」という感じです。最初から江添さんのC++11解説を読めばよかった……。
★その後、「cppreference.com」サイトの「Language~Declaration」のページにも、「Declaration specifiers (decl-specifier-seq) is a sequence of the following whitespace-separated specifiers, in any order: 【訳】宣言指定子列(decl-specifier-seq)とは、次の、空白で区切られた指定子の列であり、任意の順序です」という記述があることに気づきました。(強調引用者)
「規格書の解説に書いてあるのなら、その元の規格書にも書いてあるんじゃないの?」と思って、上で引用したJISの『プログラム言語C++』(2003)をあらためて見直し、また江添さんの解説の元ネタである「Working Draft, Standard for Programming Language C++」(N3337、2011)の指定子の節も新たに調べてみたのですが、指定子の記述順についての記載はやはり見当たりません。「順序の記載がない=順序は決まっていない」と考えてしまっていいということなのか、あるいは別の箇所にちゃんと書いてあるのに私が見つけ損なっているだけなのか疑問は氷解したとはいえ、この点だけが心残りとなっています。
江添亮「C++11の文法と機能」
・指定子の順番には、意味がない。(……cppreference.comにも記載あり)
ゆえに、指定子のconstはどこに書いてもよい。これにて一件落着!

SQLite覚え書き

SQLiteをC/C++で扱う方法については、公式サイトの「SQLite In 5 Minutes Or Less(5分以内でSQLite)」「An Introduction To The SQLite C/C++ Interface(SQLiteのC/C++インーターフェース入門)」をはじめとして、ネット上では読み切れないほどたくさんの情報が見つかります。ただ、情報が多いというのも善し悪しで、自分がやりたいことがなかなか探し出せないという事態にも繋がりましたので、プログラミングから離れている時間の方が圧倒的に長い自分の備忘のためにも、ここで整理しておきたいと思います。
sqlite3_stepとsqlite3_execのどちらを使うべきか
sqlite3_stepで複数のSQL文を連続で処理するには
sqlite3_step版を簡易クラス化する
sqlite3_step版簡易クラスのテスト
公式のコードを元にsqlite3_execを試す
sqlite3_exec版を簡易クラス化する
sqlite3_exec版簡易クラスのテスト

sqlite3_stepとsqlite3_execのどちらを使うべきか

「sqlite3_step」と「sqlite3_exec」の比較
sqlite3_stepのメリット
・プリペアドステートメントによる値のバインド処理が使える。
sqlite3_stepのデメリット
・SQL文は一度に一つしか実行できないので、複数連続で処理するならループが必要。
・前後の処理を書かなければならないのでコードが長くなりがち。
sqlite3_execのメリット
・複数のSQL文を一度に扱える。
・三つの処理をまとめたラッパーなのでコードが短くなる。
sqlite3_execのデメリット
・プリペアドステートメントによる値のバインド処理が使えない。
・SELECT用にcallback関数を書かなければならない。
一長一短ですが、sqlite3_execはプリペアドステートメントでの値のバインドができませんので、プレースホルダのあるSQLの場合は使うことができません。プレースホルダを使わないのであればsqlite3_execの方が簡単です。しかし、もしすべてのSQL文に対応できるような関数やクラスを作るのなら、機能に制限のないsqlite3_stepの方を使うべきでしょう。
★公式サイトの「5分以内でSQLite」のページには、sqlite3_execのサンプルコードしか載っていません。

sqlite3_stepで複数のSQL文を連続で処理するには

sqlite3_execなら、SQL文をまとめた文字列を渡せばそれだけで連続して実行してくれますが、sqlite3_stepではループ処理を書かなければなりません。この場合、sqlite3_prepare_v2の第5引数を使うことによって、連続したSQL文の分解をSQLiteに任せることができます。
公式の説明によると、「pzTail(=第5引数)がNULLでない場合、pzTailはzSql(=第2引数)の最初のSQL文の終端を通り過ぎた、その最初のバイトを指すようにされます。このルーチンは、zSqlの最初の文をコンパイルするだけなので、*pzTailはコンパイルされずに残っているものを指したままになります」とのこと。つまり、sqlite3_prepare_v2は、渡されたポインタsqlから文字列を読み始め、SQL文の最初の終端と思われる箇所までを処理し、その次の文字へのポインタを第5引数に格納して処理を終える、ということです。よって、上記のようなループ(while (*sql))を書いておけば、文字列へのポインタsql=読み取るべき先頭位置を、一文処理するごとに後ろにずらしつつ、SQL文群の終端('\0')に達するまで、SQL文の切り分けと処理を繰り返し実行できるわけです。
では、少し実験してみましょう。次のようなコードを書きます。
★CDデータベースのときとはループの仕方を変えています。こちらではsqlite3_stepのエラーも拾うつもりだからです。
★SELECT文で結果が返る場合にsqlite3_stepが返す文字列はunsigned char const*型なので、reinterpret_castを使ってchar const*型に変換してからでないと、string型変数に代入できません(46、7行目あたり)。また、sqlite3_column_textは、データがNULLだった場合はNULLポインタを返すので、NULLポインタでなかった場合は返った値を、NULLポインタだった場合は空の文字列を、ベクターに格納します。NULLポインタは、char const*型にキャストした後もNULLポインタのままになります。45行目のstring型変数strdataは、初期化をしていないので空の文字列が入っており、もしNULLポインタが返された場合はその空の文字列をベクターに格納します。
sqlite3_execならずっと短く書けるのに、sqlite3_stepはどうしても手数が多くなりますね。sqlite3_step周りのコードは、エラー発生時の処理をすべて省いたにもかかわらず、正味40行ほどにもなってしまいました。これをビルド&実行すると、
と表示されました。main関数内で、文字列変数sに次々とSQL文を格納し、それをそのままSqlStep関数に渡していますが、連続しているSQL文をちゃんと分離して実行してくれています。
★SELECTデータを格納しているresultdataは二次元ベクターなので、各レコードの要素数(列数)は可変対応です。上の結果のように、2列と4列が混在したレコードでも格納できます。
SQL文を一文ずつ順に取り出しているのは
の部分です。sqlite3_prepare_v2の第5引数は、このようなSQL文連続処理の場合に使うパラメーターです。
次に、連続で書かれると切り分けが少し面倒なSQL文を処理させてみましょう。main関数のSQL部分を
s += "BEGIN;";
s += "REPLACE INTO test VALUES(6, '\"I''m tired.\"''''''''''''ε-(◔ิд◔ิ;A)フゥ');";
s += "REPLACE INTO test VALUES(7, 'フォ;;ス;フォ;;フィ;;ラ;イ;ト;;');";
s += "SELECT * FROM test;";
s += "COMMIT;";
と書き換えてビルド&実行してみます。すると、
と表示されました。「おお、さすが!」という感じですw。この場合、文字列変数sの中身は
BEGIN;REPLACE INTO test VALUES(6, '\"I''m tired.\"''''''''''''ε-(◔ิд◔ิ;A)フゥ');REPLACE INTO test VALUES(7, 'フォ;;ス;フォ;;フィ;;ラ;イ;ト;;');SELECT * FROM test;COMMIT;
という状態になっているはずです。これだけ意地悪くセミコロンやらシングルコーテーションやらエスケープやらが入り混じった文字列でも、完璧に切り分けて正しく実行してくれています。
★SQLiteでは、文字列としてのセミコロンは特にエスケープの必要はありません。シングルコーテーションの囲み内にそのまま書けばOKです。しかし、シングルコーテーションを文字列として扱いたいのなら、「''」と二つ並べる必要があります。なお、上の例でダブルコーテーションをエスケープしているのは、SQLiteではなくC++の方の要求によります。
このままでも面白いけれど、少々見苦しくもあるので、やはり6番は正太郎くんに、7番は余計なセミコロンを削除したフォスに戻しておきましょう。
s += "BEGIN;";
s += "UPDATE test SET name = '金田 正太郎' WHERE id = 6;";
s += "UPDATE test SET name = "
     "(SELECT replace(name, ';', '') FROM test WHERE id = 7) "
     "WHERE id = 7;";
s += "SELECT * FROM test;";
s += "COMMIT;";
さて、次はプレースホルダのあるSQL文に対応するコードを追加します。これはsqlite3_execではできない処理です。次のハイライト部分が新たに追加、または書き換えたコードです。
★SQLiteではプレースホルダに「?」「?NNN」「:VVV」「@VVV」「$VVV」の5種類の書法が使えます。しかし、上のコードではクエッションマークを用いた「?」「?NNN」の2種しか使えず、その制限はこのページのこれ以降のコードでもすべて同じです。この5種類のうち2番目から後は、プレースホルダを識別するときのための書き方です。Nは整数、Vは英数字です。例えば、二つのプレースホルダにおいて、
SELECT * FROM test WHERE id > ?100 AND name LIKE ?4;
というように「?」に続いて適当な番号を振って、何らかの方法で値のバインド先を「%さ% → ?4」「3 → ?100」と指定することができれば、「%さ%」は「?4」に、「3」は「?100」に当てはめてくれます。
ただし、上のコードでは、プレースホルダ番号変数phnの初期値を「1」、次からの番号をそのインクリメント(phn++ →「2」「3」「4」……)としていますので、上のSELECT文のように「?100」「?4」と書いたら意味がありません。上のコードにおいては、プレースホルダが二つなら有効な番号は「1」と「2」だけになりますから、「?1」「?2」、あるいはその数字の左側に任意の数だけ0を付けた「?001」「?0000002」などと書く必要があります(このパディング用の0は何個付けても大丈夫なようです)。また、上のコードでは、連続する複数のSQL文でプレースホルダ番号を打ちたい場合、下記のようにSQL文一文ごとに1始まりとする必要があります。「?」が単独で使われたとき、そのSQL文の最初の「?」番号は必ず「1」になることを考慮し、SQL文が切り替わるたびにプレースホルダ番号変数phnの初期値を「1」にセットし直しているためで、連続しているからといって通し番号を打つと正しい結果になりません。
SELECT * FROM test WHERE id > ?2 AND name LIKE ?1;
SELECT * FROM test WHERE id < ?002 AND name GLOB ?00001;
……「3」からではなく再び「1」からスタート。
%さ% …… ?1へ
3 …… ?2へ
*里* …… ?00001へ
5 …… ?002へ
SQLiteでは、プレースホルダ番号の下限値は「1」、上限値はSQLITE_LIMIT_VARIABLE_NUMBER=「32766」だそうです(これはデフォルト値で、これより小さい数にならsqlite3_limitを使って変更可能)。よって、プレースホルダのインデックスをプログラミング的な「ゼロベースト(ゼロオリジン、0始まり)」にすることはできないようです。なお、SQLite公式では「クエッションマークの数を間違えやすいため」「?」単独の使用は推奨されておらず、これ以外の4種を使うことが推奨されています。
値格納用のベクター(vecval)を新たに宣言し、そこに使用する順に値を格納して、それを受け取れるよう引数を増やしたSqlStepを呼んで処理します。32行目のsqlite3_sqlは、プリペアドステートメントをCの文字列に変換する、すなわち元の文字列型に戻す関数です。切り分けられて今現在対象になっているSQL文一文を、それを使ってstring型変数に格納し、プレースホルダ「?」があるかどうかを調べ、あるなら値を順にプリペアドステートメントの方にバインドします。つまり、走査は元の文をコピーしたstring型変数の方で行い、値のバインドはコピー元のプリペアドステートメントの方で行うわけです。main関数のSQLでは、灯里はアリシアさんに交替してもらうことにして、その状態のテーブルで値「太郎」「フ」「a」のあいまい検索をかけています。結果は
となりました。正しい検索が行われています。もし大文字と小文字を区別したいのであれば、LIKEの代わりにGLOBを使うことで可能になります。GLOBのワイルドカードは、%ではなく*を使います。

sqlite3_step版を簡易クラス化する

以上で最低限必要なことはできるようになったので、次はこれを、使いやすくなるようクラス化したいと思います。同じようなものを「CDデータベース覚え書き」の方にも書きましたが、あちらはwxWidgets用に特化したものでしたから、こちらでは標準C++用に書いてみます。以下は方針です。
SQL文(複数可)実行は、次のいずれでもできようにする。オブジェクトをq、データベースファイル名をdb名、SQL文の集合体をsql群、プレースホルダにバインドする値をvalword、その集合体をベクターに一つずつ入れたものをval群とする。
メンバ関数SqlExec
  1. q.SqlExec(db名=string, sql群=string);
  2. q.SqlExec(db名=string, sql群=string, valword=string, valword=string, valword=string……);
  3. q.SqlExec(db名=string, sql群=string, val群=vector);
★sql群とval群を両方ともベクターにするパターンもあります。とはいえ、上で実験したとおり、SQLiteは一つの文字列変数に格納された連続するSQL文を正確に切り分けてくれますので、sql群をベクターに格納する必要性を感じません。書こうと思えば簡単に書けるけれども、ここでは省略したいと思います。
SELECTで返る結果は、それを格納した二次元ベクターメンバ変数のゲッタで取り出せるようにする。
プレースホルダへの値のバインドは、すべてsqlite3_bind_textを用いる。テキスト型なら値が数値型であっても対応できるからである。もし数値型の値を使う必要が生じた場合は、キャストで対応する。
★sqlite3_bind_xxxには、ほかにsqlite3_bind_int、sqlite3_bind_double、sqlite3_bind_blob、sqlite3_bind_null、sqlite3_bind_value、sqlite3_bind_pointer、sqlite3_bind_zeroblobなどがあり、それぞれ引数が異なります。
まず、1.と2.のケースを処理するため、SQLを実行するのに必要なデータベースファイル名・SQL文群・値群をすべて格納するベクターメンバ変数vecstr(下図参照)と、可変長引数のSqlExec関数を宣言します。
ベクターメンバ変数
下のコードのように書くとなぜ可変長引数が処理できるかは、「CDデータベース覚え書き」の方をご覧ください。
★このクラスでは、初期化する必要のあるメンバ変数が特にないので、明示的なコンストラクタは書きません。
↓実行結果
a b c d e
1 2 3 4 5 6 7 8 9 10 11 12
test.db BEGIN;DROP TABLE IF EXISTS test;CREATE TABLE test(id INTEGER PRIMARY KEY, name);INSERT INTO test VALUES(1, '大空 ひばり');INSERT INTO test VALUES(2, '水無 灯里');INSERT INTO test VALUES(3, '暁美 ほむら');INSERT INTO test VALUES(4, '赤血球 AE3803');INSERT INTO test VALUES(5, '巽 幸太郎');SELECT * FROM test WHERE name LIKE ?;SELECT * FROM test WHERE name LIKE ?;COMMIT; %太郎% %AE%
引数はこの文字列のみ
メンバ関数SqlExecは、引数がいくつあってもそれらをメンバ変数vecstrに格納できます。実際には上のオブジェクト r のように、第1引数にはデータベースファイル名を、第2引数にはSQL文の塊を、第3引数以降には値を記入します。
次に、SqlExec関数をもう一つオーバーロードして、第3引数vector版を作ります。引数の個数が固定されていないと、プレースホルダの数=値の数が不定である場合に対応できないからです。第3引数をvectorにしてしまえば引数は3個に固定できるので、文字列変数に不定数のSQL文を詰め、ベクター変数に不定数の値を詰め、しかる後にそれぞれを第2引数と第3引数に渡せば処理できます。
★例えば「SqlExec(s, vecval...)」みたいに「...」を書けばベクター変数vecvalを引数カッコ内に展開してくれる、とかいうのなら上記の可変長引数版でも対応できますが、どうやらできないようなので、値数不定の場合に特化したコードを書くほかないようです。
次のコードには、外部ファイルを読み込む関数「sqlfileread」を入れておきました。テキストファイルを読み込んで、その中に書かれたファイル名・SQL文・値を変数に格納するものです。下記のようにフラグ機能と、テストで使いやすくなるようごく簡単なエスケープ機能を付けることにします。
  1. 行の文字列が「//--file」だけならば、その下からはファイル名。
  2. 行の文字列が「//--sql」だけならば、その下からはSQL文。
  3. 行の文字列が「//--val」だけならば、その下からは値。
  4. 行の文字列が「//--0」だけならば、その下からは読み込まない。
  5. 行の文字列が「//--1」だけならば、その下からは読み込む。
  6. 「/*」があったら、「*/」が出てくるまで読み込まない。
  7. 「//」があったら、その後その行は行末まで読み込まない。
  8. 「\/」があったら、「/」は文字列扱いとする。
    ……「\//」なら、「//」もエスケープされず文字列となる。
  9. 改行だけの行は無視する。
  10. 「//--val」以下で、行の文字列が「//--blank」ならば、空の文字列とする。
4.5.は、「#if 0」「#if 1」と同じように使えます。10.は、9.により改行だけの行が飛ばされてしまうため、値に空の文字列が欲しい場合に使います。例えば、「特定の文字列を削除する」=「特定の文字列を空の文字列に置き換える」ような場合です。
↓sqlval.txtの中身

//--file
test.db

//--sql
SELECT * FROM test WHERE name LIKE ?;
SELECT * FROM test WHERE name LIKE ?;
SELECT * FROM test WHERE id = ?;
SELECT * FROM test WHERE id = ?;
SELECT * FROM test WHERE id = ?;

//--val
%太郎%
%AE%
3
2
1
↓実行結果
test.db
SELECT * FROM test WHERE name LIKE ?;SELECT * FROM test WHERE name LIKE ?;SELECT * FROM test WHERE id = ?;SELECT * FROM test WHERE id = ?;SELECT * FROM test WHERE id = ?;
%太郎%
%AE%
3
2
1
──要素数は 7
コメントのせいでずいぶん長くなってしまいました(これくらいコメントを書き込んでおかないと自分の場合覚え書きにならないので……w)。このクラスでは、ベクターであるメンバ変数vecstrの[0]にデータベースファイル名を、vecstrの[1]にSQL文の連続文字列を、[2]以降にプレースホルダ用の値を格納してから処理する仕様になっています。ですから、先の
①SqlExec(db名, sql群, val, val, val……) ←すべて文字列
であれば、単純にvecstrに順に詰めていけばいいのですが、今回の
②SqlExec(db名, sql群, val群) ←val群はベクター
になると、val群の方は一つ一つ取り出しながらvecstrに詰めていく必要があります。今回のSqlExecオーバーロード版ではその処理が加わっています。
で、なぜ②があった方がいいのかというと、上の実験のように外部ファイルからSQL文と値を読み込むような場合、つまりプログラムコード内に固定的なSQL文を書くのではなく、外部からの問い合わせがあってその都度SQLが変化するような場合、プレースホルダがいくつあるのか=値がいくつあるのかが事前には分からない可能性が高いからです。もしそういうケースが生じた場合、①では対応できません。
新しいクエリが来た!
①q.SqlExec(db, sql, val1, val2, val3, val4, val5……) ──引数をいくつ用意すればいいのか?
②q.SqlExec(db, sql, vval) ──vvalにはいくつでも値をpush_backできるし、引数は3個固定なのでこの書式だけで対応できる。
ここで、sqlval.txtの中身を下記のように書き換えて、ビルドはせずに、先に作ったのと同じ実行ファイルで実行してみましょう。
↓sqlval.txtの中身

//--file
test.db

//--sql
UPDATE test SET name = ? WHERE id = ?;
SELECT * FROM test WHERE name LIKE ?;
SELECT * FROM test WHERE name GLOB ?;

//--val
源 さくら
5
%郎%
*ら*
↓実行結果
test.db
UPDATE test SET name = ? WHERE id = ?;SELECT * FROM test WHERE name LIKE ?;SELECT * FROM test WHERE name GLOB ?;
源 さくら
5
%郎%
*ら*
──要素数は 6
メンバ変数vecstrの中身が変化しています。対応できていますね。vecstrがこの内容であるならば正しく処理できるはずです。
★②があれば①は不要であるとも言えます。ただ、値文字列を引数内に単純に並べるのと、ベクター変数を作ってpush_backしてから引数に書き込むのとでは、わずかではありますが労力が異なります。①で書く方が楽でしょう。
★CDデータベースでは、SQLは固定的ですべてソースファイル内に記載していますので、①だけで対応可能です。
これでだいたいの枠組みは作れましたので、上で作ったSqlStep関数を組み込みます。ここでsqlite3.hをインクルードするので、今後はreturnに定数SQLITE_OK(0)とSQLITE_ERROR(1)を使うことにします。また、新規でデータベースを作る関数も加えました。
新しい関数と、上で省略していたエラー処理も入れたので、だいぶ長くなってしまいました。それでも構造は単純で、
①SqlExec関数でdb名とSQL文と値をメンバ変数vecstrに格納する。
②SqlStep関数でSQL文を処理し、結果が返る場合はメンバ変数resultdataに格納する。
と、ただこれだけのことをやっているにすぎません。190行目くらいまでがこのクラスの中枢で、その下はおまけです(クラスの中でも、結果を表示するメンバ関数resultdispは、どちらかというとおまけです)。

sqlite3_step版簡易クラスのテスト

それでは、テストしてみましょう。上で作ったデータベースファイルは捨てるか移動するかリネームするかして、「test.db」というデータベースファイルが実行ファイルと同じフォルダに存在しないようにしておきます。そして、main関数に次のように書き込みます。
ここで、わざと入力キーを誤ったり、新規作成をためらったりしてみます。
新規のデータベースファイルが作られ、SQLが実行されました。
ここでちょっと実験してみます。完成版コードの47~50行目を
このようにコメントアウトし、main関数を次のように変更します。
データベースファイルがすでに生成されていますが、わざとファイル名を空白にして開くようにします。そして、ひばりくんの名前はお父さんのいばりさんに変えて、SQL文中最下行にあったファイルタイプ付きのSELECT文は消しておきます。これでビルド&実行してみると、
正常終了してしまいましたね。ファイル名を空白にしたのに次に、完成版コードの47~50行目を元に戻し、main関数を次のようにしてビルド&実行してみます。今度はちゃんとさっき作ったファイルの名前を指定して、SQL文は一文だけとします。
1をお父さんの名前に変更したはずなのに、ひばりくんに戻ってしまいました。これはどういうことでしょうか?
実は、SQLiteは、ファイル名を空白にすると、テンポラリデータベースを作ってそちらで処理しようとする仕様となっています。「テンポラリ」というくらいですから、そのデータベースは処理が終わり次第破棄されます(おそらくSQL文のテスト用に用意された機能だと思われる)。今回は最初にテーブル作成をしていますので、テンポラリデータベースに新しいテーブルが作られ、その後もその新しいテーブル対象のSQL文しかなかったがために、問題なく処理が完了してしまったというわけです。もしSQL文が最後の「s += "SELECT * FROM test;";」だけだったならば、テンポラリデータベースに新しいテーブルが作られていない状態なので、「そんなテーブルはない」というエラーメッセージが出て異常終了したはずです。このように、SQLiteはデータベース名が空白でも処理が成功してしまうことがあるので、注意が必要です。「ファイル名が空白です」のチェックが必要である所以です。
さて、テストを続けるにしても、main関数を書き換えながら行うとなるといちいちビルドしてから実行しなければならず面倒なので、SQL文はsqlval.txtファイルの方で書き換えることにして、コードの方は固定しておきたいと思います。main関数を次のように書き換えます。
では、クラスを作る前に実験したSQL文を実行してみましょう。上記コードをビルドしてから、sqlval.txtを下記のように書き換えて実行します。
↓sqlval.txtの中身

//--file
test.db

//--sql
SELECT * FROM test WHERE name LIKE ?;
SELECT * FROM test WHERE name GLOB ?;
SELECT * FROM test WHERE name LIKE ?;
SELECT * FROM test WHERE id = ?;
SELECT * FROM test WHERE id = ?;

//--val
%太郎%
*AE*
%ら%
2
1
idはinteger型で、値のバインドにはsqlite3_bind_textを使っているにもかかわらず、特に数値にキャストしなくても大丈夫そうです。
★ただし、これはid列にINTEGER PRIMARY KEYを指定したためであるようです。
次はsqlval.txtを下記のように書き換えて、この後はビルドはもうせずに、実行だけをします。
↓sqlval.txtの中身

//--file
test.db

//--sql
UPDATE test SET name = ? WHERE id = ?;
SELECT * FROM test WHERE name LIKE ?;
SELECT * FROM test WHERE name GLOB ?;

//--val
源 さくら
5
%太郎%
*ら*
最初に巽をさくらに入れ替えているので、「太郎」のあいまい検索ヒットが正太郎くんだけになり、「ら」のあいまい検索ヒットがほむらとさくらの二人となりました。次は、セミコロンやシングルコーテーションが入り乱れたテキストの挿入です。
↓sqlval.txtの中身

//--file
test.db

//--sql
BEGIN;
REPLACE INTO test VALUES(?, ?);
INSERT INTO test(name) VALUES(?);
SELECT * FROM test;
COMMIT;

//--val
6
"I'm tired."''''''ε-(◔ิд◔ิ;A)フゥ
フォ;;ス;フォ;;フィ;;ラ;イ;ト;;
やはりこれも大丈夫ですねw。今回はSQL文のプレースホルダ(?の位置)に後からバインドする値なので、エスケープ処理は必要ありません。もし最初からSQL文の中に書くのなら、上の方で最初に試したときのようにSQLite用のエスケープ処理が必要になります。その場合はシングルコーテーションで囲むことになるからです。なお、C++用のエスケープ処理は、ソースコード内に書くのでなければ不要です。
★フォスを挿入しているINSERT文では、「INSERT INTO test(name) VALUES(?);」とidを指定していませんが、正しく7番目に挿入されました。これは、idカラムがINTEGER PRIMARY KEYに指定してあるため、連番機能が働いたのです。
では正太郎くんを戻し、フォスの名前を正しい表記にUPDATEしますが、replaceを含むSQL文は長いので改行してみます。
↓sqlval.txtの中身

//--file
test.db

//--sql
BEGIN;
UPDATE test SET name = ? WHERE id = ?;
UPDATE test SET name = 
(SELECT replace(name, ?, ?) FROM test WHERE id = ?) 
WHERE id = ?;
SELECT * FROM test;
COMMIT;

//--val
金田 正太郎
6
;
//--blank
7
7
大丈夫ですね。SQL文が改行されていようとも、一つの文字列変数に改行なしで次々と詰め込んでからSQLiteに渡すので、問題はありません。上記のように、「//--blank」は、値に空の文字列を指定したい場合のために用意した文字列並びです(改行だけだとその行は無視される)。「//--val」の範囲内でのみ有効になります。「//--blank」と書いてあっても、SQLiteには実際には「」(空の文字列)が渡されています。
次は、わざとエラーを起こして反応を確認してみましょう。ほむらはいろはに、赤血球は好中球に入れ替えます。
↓sqlval.txtの中身

//--file
test.db

//--sql
BEGIN;
UPDATE test SET name = ? WHERE id = ?;
REPLACE INTO test VALUES(?, ?);
SELECT * FROM test;
COMMIT;

//--val
環 いろは
3
4
白血球 U-1146
まず、よくやりがちな、途中のSQL文の終わりにセミコロンを付け忘れた場合。REPLACEの行の「;」を消して動かしてみます。
REPLACE INTO test VALUES(?, ?)   // ←「;」を消す。
SQLiteのエラーメッセージってこういう感じなんですね。あんまり見たことがなかった気がします。思惑どおりエラーになりましたが、制御はmain関数に戻っています。エラーを吐いたのはsqlite3_prepare_v2で、ここで構文チェックが行われたことが分かります。ここで、次のように「SELECT * FROM test;」だけを実行します。
↓sqlval.txtの中身

//--file
test.db

//--sql
SELECT * FROM test;

//--val
2番目のREPLACE文がエラーになったのですが、上のUPDATE文も実行されていません。トランザクションが働いたことが分かります。次に、先のSQL文と値を戻し、今度はREPLACEの行の「;」を元に戻し、UPDATE文の方の「name」を「nama」と書き換えてみます。
UPDATE test SET nama = ? WHERE id = ?;  // ←「nama」にする。
REPLACE INTO test VALUES(?, ?);   // ←「;」を戻す。
エラーになりました。次に、今の「name」を元に戻してから、REPLACE文の「test」を「testt」と書き換えてみます。
UPDATE test SET name = ? WHERE id = ?;  // ←「name」に戻す。
REPLACE INTO testt VALUES(?, ?);   // ←「testt」にする。
エラーになりました。次に、今の「test」を元に戻してから、いちばん上の「BEGIN」の行をコメントアウトしてみます。
//BEGIN;        // ←コメントアウトする。
UPDATE test SET name = ? WHERE id = ?;
REPLACE INTO test VALUES(?, ?);    // ←「test」に戻す。
エラーになりましたが、今回初めてsqlite3_stepがエラーを吐きました。「トランザクションがアクティヴでないのでコミットできない」と言っていますね。「4 文目でエラー発生~3 文目までは実行済」ということは、今はBEGINをコメントアウトしているので、最後のCOMMIT文でエラーになったということになります。そのCOMMIT文も、構文自体は間違っていないので、sqlite3_prepare_v2の方は問題なしということで通過したのでしょう。結果を見ると、3のいろはも4の好中球も更新されていますから、大勢に影響はないように見えます。とはいえ、COMMITがエラーになったということは、まだ確定されていない可能性もあります。その確認のため、下記のようにSELECT文のみを発行してみます。SQLiteは、データベースファイルが開かれるときにジャーナルファイルが残っているかどうかを調べ、もしあればそれを読み込みます。ですので、ここでSELECT文を発行した場合、もしジャーナルファイルが残っていたならば前の状態に戻るはずです。
↓sqlval.txtの中身

//--file
test.db

//--sql
SELECT * FROM test;

//--val
元に戻りませんね。フォルダの中を目で確認しても、ジャーナルファイルは残っていないようです。ということは、トランザクションは完了し、更新は確定されたということになります。COMMITはエラーになったのにトランザクションが完了しているのはなぜなのか。これは、BEGINがなかったために各文に暗黙のトランザクションが働いたからであると考えられます。
(BEGIN;)
UPDATE test SET name = ? WHERE id = ?;
(COMMIT;) ……ここでUPDATE文確定。
(BEGIN;)
REPLACE INTO test VALUES(?, ?);
(COMMIT;) ……ここでREPLACE文確定。
(BEGIN;)
SELECT * FROM test;
(COMMIT;) ……ここでSELECT文確定。
COMMIT; ……BEGINがないのでsqlite3_stepエラー。
では、今度は逆にBEGINを元に戻してCOMMITの方をコメントアウトし、ひばりくんを妹のすずめちゃんに、正太郎くんは大作くんに変更してみます。
↓sqlval.txtの中身

//--file
test.db

//--sql
BEGIN;
UPDATE test SET name = ? WHERE id = ?;
REPLACE INTO test VALUES(?, ?);
SELECT * FROM test;
//COMMIT;

//--val
大空 すずめ
1
6
草間 大作
エラーもなく更新されました。しかし、ここで先のようにSELECT文だけを発行してみると、
元に戻ってしまいました。更新は確定されていなかったことになります。フォルダを見ると、今度はジャーナルファイルが残っていました(test.db-journal)。どうやら、BEGINだけでCOMMITもROLLBACKもない、すなわち完了していないトランザクションは、停電やシステムダウンなどの事故と同じように扱われるようです。復元のために残されていたジャーナルファイルは、その次の「更新」が行われるときにようやく削除されます(「選択」ではなく)。試しに、フォスの代わりにシンシャに入ってもらうREPLACE文を実行してみましょう。
↓sqlval.txtの中身

//--file
test.db

//--sql
REPLACE INTO test VALUES(?, ?);
SELECT * FROM test;

//--val
7
シンシャ
正しい更新が行われ、フォルダからはジャーナルファイルが消滅しました。
以上のことから、更新処理においては、もしBEGINを書いたのならば、必ずCOMMIT(あるいはROLLBACK)を書いておく必要がある、ということが言えそうです。
★SELECT文のみの場合、BEGINを書いてCOMMITを書かなかったとしてもジャーナルファイルは作られません。また、SELECT文のみで、BEGINを書かずにCOMMITを書いた場合は、やはりエラーになります。となると、更新処理でない場合は、明示的なトランザクションは書かない方が無難ということになるでしょうかおそらく、ほかのユーザーの更新割り込みがなく、SELECT文が何十個も連続しないのであれば、SELECT文の明示的なトランザクションは必要ないと思われます。逆に、ほかのユーザーの更新割り込みが起こり得る場合は、トランザクションを書かなければ「分離レベル」で説明されるような不具合が頻発するでしょうし(書いても分離レベルによっては発生することがある)、SELECT文がたくさん連続する場合は、トランザクションを書かなければ個々に暗黙の読み取りトランザクションがかかる分、処理が遅くなるでしょう。
さて、本稿はSQLiteのテストを行うことは目的ではないので、次はクラスに装備したエラー処理の挙動を確認します。
先ほどの変更は元に戻ってしまったので、もう一度すずめちゃんと大作くんに入ってもらいますが、試しにプレースホルダの数と値の数が合わないようにしてみます。「1」と「6」を下記のようにコメントアウトします。
↓sqlval.txtの中身

//--file
test.db

//--sql
BEGIN;
UPDATE test SET name = ? WHERE id = ?;
REPLACE INTO test VALUES(?, ?);
SELECT * FROM test;
COMMIT;

//--val
大空 すずめ
//1
//6
草間 大作
プレースホルダ4個に対して値が2個しかないのでエラーになりました。これは日本語のメッセージなので、上のプログラムコードに書き入れたエラーチェックに引っかかったことが分かります。
★このプログラムでは、もしプレースホルダの数よりも値の数の方が多かったとしても、バインドされた値が適切ならそのまま正常終了します。処理の過程で、SQL文と値を格納したベクターメンバ変数からプレースホルダの数だけ値を取りにいきますが、SQL文の最後に達したとき、ベクターの中に使わなかった値があったとしてもそのままになります。こういう「値の使い残し」に対するチェック機能を付けていないからで、いわば沈黙のエラーです。逆に値の方が少ないと、ベクターから取り出そうとしても取り出せなくなるため、その時点でout_of_rangeとなります(添字ではなくatを使っているので、out_of_rangeは必ず表面化します)。この両方を同時に避けるために、最初に?の数と値の数をチェックするようにしました。今回のように、SQL文とバインドする値を何度も書き換えるようなケースでは、このようなチェックも役立つでしょう。
次に「1」「6」を戻してファイル名をコメントアウトしてみると、
//test.db  ←コメントアウト。

~略~

大空 すずめ
1        // ←元に戻す。
6        // ←元に戻す。
草間 大作
次にコメントアウトを戻してからファイル名を変えると、
test123.db  // ←コメントアウトを戻して改名。
となります。新しいデータベースは必要ないので「n」で中断しましょう。次にファイル名を戻してから、SQL文を全部コメントアウトすると、
//--file
test.db        // ←元の名前に戻す。

//--sql        // ↓すべてコメントアウトする。
/*BEGIN;
UPDATE test SET name = ? WHERE id = ?;
REPLACE INTO test VALUES(?, ?);
SELECT * FROM test;
COMMIT;*/
大丈夫そうですね。では、今のコメントアウトを削除して、sqlval.txtの中身を最初の内容に戻してから実行してみます。
今度は正しく更新されました。

公式のコードを元にsqlite3_execを試す

ここからは、もう一つのSQL実行コマンドであるsqlite3_execについて書いてみたいと思います。
上記のように、sqlite3_execは簡単で使いやすいし、SQLiteの公式ページにC用のサンプルコードが載っていますので、もしSQLにプレースホルダを一切使わないのであればこちらを使うのが得策です。
では、試しに公式ページのサンプルコードを使ってtest.dbにアクセスしてみましょう。ただし、サンプルコードはCで書かれていますので、それをC++用に直し、かつコマンドライン引数を使わない形にしてみます。コマンドライン入力でSQL文を打つのは面倒そうだし、SQL文中の値に日本語が混じるとうまくいかないことがあるので、この方が無難でしょう。
★この例ではcallback関数の第1引数を使っていませんので、コンパイラの警告レベルによっては「not used」「unused」といった警告が出ます。いちばん上の「if (NotUsed != nullptr) {return 1;}」の1行は、それを防止するためだけに書いた実質的には意味のない処理ですから、警告が出ないのであれば書く必要はありません。
★sqlite3_execのエラーの処理は、すぐ上のsqlite3_openや先のsqlite3_stepなどと違って、①エラーメッセージ用文字列のポインタを宣言して、②それが使われた場合は解放する、という手続きになっています。Cでmallocを使うときと同じような手順ですね。
ちゃんと動きました。いや~、実に簡単でいいですね公式サイトのコードですから、これを真似しておけば間違いないという安心感もあります。今はSQL文を一つしか書きませんでしたが、sqlite3_execは複数のSQL文に対応していますので、sqlite3_stepのときのようなループを書かなくても、連続するSQL文を渡しておけば適切に切り分けて順に実行してくれます。例えば上のコードのSQL文を次のように書き直してみましょう。引数は2個限定ですので、複数のSQL文を一つの文字列にまとめて渡します。
正しい結果が返りました。

sqlite3_exec版を簡易クラス化する

それでは、これも簡単なクラスにしてみたいと思います。sqlite3_execではプレースホルダへの値のバインドはできないので、その部分は削ります。
クラスの中枢をsqlite3_execで作る場合、SELECTで返ってきたデータ、すなわちcallback関数に投げられるデータを、どのようにメンバ変数に格納するかが問題となります。callback関数はメンバ関数にすることができないからです。しかし、sqlite3_execの第4引数とcallback関数の第1引数を使うことで、SELECTで返った結果をクラス内に簡単に取り込むことができます。下記のように、sqlite3_execの第4引数にthisポインタを指定しておけば、callback関数の第1引数がそれを受け取って、this=オブジェクトへの接続が可能になるというわけです。
sqlite3_execの第4引数とcallback関数の第1引数はどちらもvoidポインタ、すなわちユーザーが自由に使えるパラメーターです。これらは、おそらくこういった用途のために用意されているのでしょう。
それでは、先のsqlite3_step版のコードに変更を加える形で、sqlite3_execを中枢に用いたクラスを作ってみましょう。変更箇所はコメントで指摘してあります。
★callback関数からデータを渡されるDBcallback関数では、カラム名を格納した変数azColNameを使っていません。よって、ここでもコンパイラの「引数未使用警告」対策コードを書いておきました。コンパイラを欺くためだけのコードですから、警告が出ないのであればもちろん不要です。カラム名をもし使うとしたら、二次元ベクターのいちばん上に一度だけpush_backしておいて、表示するとき最上行にカラム名が来るようにしておく、といったところでしょうか。……そう言えば、なでしこでSELECT結果を配列に格納すると、いちばん上に常にカラム名が挿入されていました。最上行がカラム名欄である《グリッド》に表示させる場合はそうでないと困るのに対し、《リスト》に表示させる場合はカラム名は邪魔だったので、後者の場合はいちいち「検索結果の0を配列削除」と書いていた記憶があります。

sqlite3_exec版簡易クラスのテスト

さて、こちらも何かテストをしておきたいのですが、sqlite3_exec版ではプレースホルダへの値のバインドができないので、SQL文の値の箇所がテキストの場合はシングルコーテーションで囲んだ具体的な値を書いておかなければなりません(数値ならシングルコーテーションは不要)。シングルコーテーションを使うとなるとエスケープ文字にも気を配らなければならないので、少々面倒です。まあプレースホルダが使える場合でも、「?」が大量にあって値を別に書くとなると、どれがどこに当てはまるのか対応関係が分からなくなりそうなので、一長一短でしょうか。
では簡単に。まず次のSQL文をsqlval.txtに書き、実行します。データベースファイルは新しく作ります。プレースホルダが使えないので、SQL文には「?」を書くことができず、また//--valから下には何を書いても無視されます。
↓sqlval.txtの中身

//--file
testnew.db        // ←新しいファイル名にする。

//--sql
BEGIN;
DROP TABLE IF EXISTS test;
CREATE TABLE test(id INTEGER PRIMARY KEY, name);
INSERT INTO test(name) VALUES('白い犬の燃えた日');
INSERT INTO test(name) VALUES('ゆうれい談');
INSERT INTO test(name) VALUES('バナナブレッドのプディング');
INSERT INTO test(name) VALUES('ビブリオテーク・リヴ');
INSERT INTO test(name) VALUES('姫のためなら死ねる');
SELECT * FROM test;
COMMIT;

//--val
//sqlite3_exec版ではこの下は無効なので使わない。
正常に実行されました。次は名前を昇順で並べ替えてみましょう。
↓sqlval.txtの中身

//--file
testnew.db

//--sql
SELECT * FROM test ORDER BY name ASC;

//--val
大丈夫ですね。
次は、sqlite3_exec簡易クラスのテストからはちょっと離れてしまいますが、nameカラムに数値型のデータを入れてみたいと思います。値をシングルコーテーションで囲まない場合は数値扱いになります。SQL文中のシングルコーテーションの有無でテキスト型と数値型を区別するやり方は、プレースホルダを使った場合はできません。
↓sqlval.txtの中身

//--file
testnew.db

//--sql
BEGIN;
INSERT INTO test(name) VALUES(123);
INSERT INTO test(name) VALUES(456.789);
SELECT id, typeof(id), name, typeof(name) FROM test;
COMMIT;

//--val
正しく挿入されました。それにしても、こんなふうに一つのカラムにtext型とinteger型とreal型が混在しているというのは、これがSQLiteの特徴とはいえ、まるで表計算ソフトのようで何だか違和感がありますね。
さて、ここでsqlval.txtファイルを次のように書いてみましょう。プレースホルダを使うので、上のsqlite3_step版簡易クラスの実行ファイルの方を指定してから実行してみます。
↓sqlval.txtの中身

//--file
testnew.db

//--sql
SELECT * FROM test WHERE id = ?;
SELECT * FROM test WHERE name = ?;
SELECT * FROM test WHERE name = ?;

//--val
1
123
456.789
id列対象の検索はヒットするのに、name列対象の検索はヒットしませんでした。これは、ここでのクラスはすべての値をsqlite3_bind_textによってバインドしているため、「123」も「456.789」も文字列として扱われてしまったためであると考えられます。数字のように見えるテキストを何もしなくても数値扱いしてくれるのは、どうやらINTEGER PRIMARY KEY指定のカラムだけのようです。こういう場合はSQL文で対応することができます。SQL文のプレースホルダ部分を「name = cast(? as integer)」「name = cast(? as real)」などとキャストするか、あるいはもっと簡単に、次のように0を加算すれば数値扱いとなります。
↓sqlval.txtの中身

//--file
testnew.db

//--sql
SELECT * FROM test WHERE id = ?;
SELECT * FROM test WHERE name = ? + 0;
SELECT * FROM test WHERE name = ? + 0;

//--val
1
123
456.789
今度は期待どおりの結果になりました。
それでは、再びsqlite3_exec版簡易クラスの実行ファイルの方に戻って、テーブルの6番目以降を削除します。
↓sqlval.txtの中身

//--file
testnew.db

//--sql
DELETE FROM test WHERE id > 5;
SELECT * FROM test;

//--val
先ほど挿入した数値の2レコードが正しく削除されました。
ここで、これまたクラスのテストとは関係ないのですけれど、ちょっと個人的に気になっていたことを実験してみます。東欧圏とロシアの人名を入れてみましょう。
↓sqlval.txtの中身

//--file
testnew.db

//--sql
BEGIN;
DELETE FROM test;
INSERT INTO test(name) VALUES('Jiří Trnka');
INSERT INTO test(name) VALUES('Reisenbüchler Sándor');
INSERT INTO test(name) VALUES('Юрий Норштейн');
SELECT * FROM test;
COMMIT;

//--val
大丈夫でした。まあこれくらい大丈夫じゃないと困るんですがw。環境がUTF-8で統一できていれば文字コードの問題は起こりません。それにしても、キリル文字はフォントによってずいぶん幅が違ってしまいますね。
★なでしこでは、SQLiteの処理の前に「SQLITE3自動変換=オン」と書いておきさえすれば、文字コードの問題を気にしなくて済みました。さすがなでしこといったところですが、Windowsメモ帳の既定の文字コードがUTF-8となった今となっては、すでに時代を感じさせるコマンドになりつつあります。

ご意見・ご教示等ございましたら こちら からお送りください。

Copyright © 2021 鷺澤伸介 All rights reserved.