C++をあまり深入りせずにBetter Cとして使うとこうなる。
事前準備:文法面の細かな差異を押さえる
細かいところにC言語とC++の非互換な部分があるので、それを押さえておく。例えば:
- 標準ライブラリのヘッダファイル名
- 例えばassert.hではなくcassert。まあassert.hでもいける処理系も多いのだが……。
- 構造体内で定義した構造体・列挙型等のスコープ
- struct Foo内で定義したstruct Barは、Foo::Barとなる。
- C言語では、この手のスコープは存在しないので……。
- 関数の宣言・定義にvoidを書く/書かないの問題。
C95以前のCプログラマ向け:ブロック先頭以外で内部変数を宣言する
C99以降ではブロック先頭以外でも内部変数を宣言できるようになったが、お仕事でC言語を使っているとC95以前が絡むことも多いので、ブロック先頭でまとめて宣言する癖がついているベテランも多いと思う。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[])
{
int i;
size_t len;
for (i = 0; i < argc; i++) {
len = strlen(argv[i]);
(void) printf("%lu\t%s\n", (unsigned long) len, argv[i]);
}
return EXIT_SUCCESS;
}
変数の寿命などの問題*1が絡んでくるので、内部変数は使う直前に宣言・初期化するようにする。
#include <cstdio>
#include <cstdlib>
#include <cstring>
int main(int argc, char *argv[])
{
for (int i = 0; i < argc; i++) {
std::size_t len = std::strlen(argv[i]);
(void) std::printf("%lu\t%s\n", (unsigned long) len, argv[i]);
}
return EXIT_SUCCESS;
}
C95以前のCプログラマ向け:bool型を使う
これも、C99以降で_Boolやstdbool.h(bool、false、true)が追加されたが、お仕事でC言語を使っているとC95以(ry
static int valid_number(const char *s);
static BOOLEAN valid_token(const char *s);
#include <stdbool.h>
static bool valid_number(const char *s);
static bool valid_token(const char *s);
static bool valid_number(const char *s);
static bool valid_token(const char *s);
ファイルスコープの代わりに無名名前空間を使う
ファイルスコープと書いたが、要はstatic関数やstaticなグローバル変数のことだ。
static int foo;
static int triple(const int n)
{
return n * 3;
}
C++的には、staticではなく無名の名前空間を使うことが推奨されている。
namespace {
int foo;
int triple(const int n)
{
return n * 3;
}
}
Better Cの観点では、この項目の優先度は低い気がしないでもない。
とはいえ、よく考えれば毎度毎度staticを書くのも面倒な訳で、コードを書く時の流儀として「ファイル先頭側に『モジュール内部で使用する非公開関数群』をまとめて記述して、ファイル末尾側に『外部に公開する関数群』を記述する」というスタイルを採用している人ならば、staticから無名の名前空間に切り替えるのは悪くない判断だと思う。
……公開関数と非公開関数を混在させるスタイルの人にとっては扱いにくいだろうけど。
C++11以降:cstdint(stdint.h)の型を使う
やや組み込み系っぽいネタ。
これ、C99以降のC言語を使う場合もそうなのだが、厳密な大きさ(ビット幅)を指定したデータ型を用いるのに、そろそろ独自のtypedefではなくint8_tやuint16_tを使っても許されるのではないかと思うのだ……。
#include "common_types.h"
#define WORK_BUF_SIZE 4096
static BOOLEAN module_initialized = FALSE;
static U16 my_port_number = 0;
static U8 packet_send_buf[WORK_BUF_SIZE];
static U8 packet_recv_buf[WORK_BUF_SIZE];
#include <stdbool.h>
#include <stdint.h>
#define WORK_BUF_SIZE 4096
static bool module_initialized = false;
static uint16_t my_port_number = 0;
static uint8_t packet_send_buf[WORK_BUF_SIZE];
static uint8_t packet_recv_buf[WORK_BUF_SIZE];
#include <cstddef>
#include <cstdint>
namespace {
constexpr std::size_t WORK_BUF_SIZE { 4096 };
bool module_initialized { false };
std::uint16_t my_port_number { 0 };
std::uint8_t packet_send_buf[WORK_BUF_SIZE];
std::uint8_t packet_recv_buf[WORK_BUF_SIZE];
}
ちなみに、C++11的にはcstdintをインクルードするべきなのだが、現時点では移植性の問題でstdint.hをインクルードした方が安全なこともあるようだ。
C++03以前:constによる定数を積極的に使う
C言語では、定数を定義する方法は2通りある。マクロ置換を使う方法と、enumの列挙子を使う方法だ。
#define FOO_LIMIT 128
enum {
FOO_LIMIT = 128
};
C++では、const修飾子を使用して整数や浮動小数点数を定数化することも可能だ。定数を定義する方法が増えている。
static const std::size_t FOO_LIMIT = 128;
const修飾子はC言語の頃から存在する。しかしC言語では、const修飾子を付けた変数は定数化せず、実質的に「ほぼreadonlyの変数」として振る舞う。変数なので、配列を定義する際に要素数として使うことが不可能な代物だ。加えて、不注意で値を変更できてしまう余地が大いにあるし、そのようなコードがコンパイル時に警告扱いで通過してしまうことが多い。そしてそんなオブジェクトコードを実行すると、運がよければ普段使いの範囲の操作で確実にアプリが落ちるし、運が悪ければアプリは落ちずに原因不明の不具合に悩まされることになる。
しかしC++では、コンテキスト次第ではしっかりと定数扱いされる。整数型なら配列定義の要素数として問題なく使えるし、値を変更するようなコードを書こうものなら十中八九コンパイルエラーとなる。
const修飾子による定数のメリットは、型を明示する必要があること*2だ。またプリプロセスではなく「C++の言語本体の仕様の一部」であるので、enumと同様にスコープの概念が有効となる。そしてenumとは異なり、整数以外の型も定数化できる。
C++で定数を定義する際は、基本的にはconstを使用して、適切な型の定数を用いるべきだろう。その上で、何らかのIDのような連番が必要な場合や、デバッグ時に変数の値を定数のシンボル名で確認したい場合に、enumを使うべきだろう。
#defineによる定数の定義は、C++では基本的には避けるべきだ──#defineを使うのは、constやenumによる定数のメリットが仇となるような例外的状況や、C言語とのインタフェース(C++で実装したモジュールをC言語に組み込むようなケース)にとどめるべきだ。
C++11以降:constよりもconstexprで定数を定義する
ところでC++03以前の「const修飾子を付与した変数」には、コンパイル時に値が決まっているもの(≒定数として扱えるもの)と、実行時に変数を初期化する時点にて値が決まるもの(≒constを付与した仮引数のようなケース)の2種類があった。
このうち「コンパイル時に値が決まっているもの」については、C++11にてconst修飾子の代わりとなるconstexpr指定子が登場した、という歴史的経緯がある。constexprを付ければ、コンパイラは「この定数の中身はコンパイル時に決まる(だからROM化できる)はず」と解釈してコンパイルを試みるはずだ。
static const std::size_t FOO_LIMIT = 128;
static constexpr std::size_t FOO_LIMIT = 128;
C++11以降では、定数の定義にはconstではなくconstexprを用いるべきだろう。
なお整数や浮動小数点数ならともかく、ユーザ定義型では「コンパイル時に全てを決めることができない」というケースも多く、その場合はconstexprで定数にすることができない。こういう場合はconst修飾子を使用して「(定数ではないが、外から見て)不変のオブジェクト」として扱うことになる。
constexprは関数にも適用できるのだが(何しろ「constant expression == 定数式」の略であるし)、Better Cではひとまず定数のことだけ覚えておけば何とかなる。C++14以降が使える環境になったらconstexpr関数のことを思い出す、ぐらいでちょうど良いのではないか?*3
C++11以降:autoで型推論させる
C++11以降では、変数を宣言する際に具体的な型を明示する代わりにautoキーワードを用いることで、初期化子から推論した型を適用させることが可能になった。
int n = 1;
const unsigned long maxval = 255;
const char *s = "";
const char *p = s;
auto n = 1;
const auto maxval = 255UL;
auto s = "";
const auto *p = s;
例えばイテレータのように、標準ライブラリの機能に付随する型については、毎度毎度自分で長ったらしい型名を書いたり、typedefなどで省略形を定義して使うよりも、autoを使ったほうが楽だろう。
const std::vector<int> TABLE {
32, 64, 96, 128, 192, 256, 512, 1024
};
std::vector<int>::const_iterator p =
std::find(TABLE.begin(), TABLE.end(), value);
if (p == TABLE.end()) {
}
const std::vector<int> TABLE {
32, 64, 96, 128, 192, 256, 512, 1024
};
auto p = std::find(TABLE.begin(), TABLE.end(), value);
if (p == TABLE.end()) {
}
もちろん全てをautoで置き換えることは(不可能である以前に)ナンセンスであるが、しかしautoを使えるシーンが色々とあることも確かである。ということで積極的に使ってよい機能だと思う。ただし統合開発環境ないし「モダンで賢いソースコードエディタ*4」は必須だろう。推論された型をエディタ上で簡単に確認できる仕組みが無いと、ちょっとやりづらい。そしてそんな「便利な開発環境」を構築して快適に利用できる程度に「開発マシンのスペック」や「ソフトウェア導入の自由度」が担保されている必要もある……。
なおC++14ではautoを使用可能なシーンが増えており、関数宣言/定義の戻り値の型名の代わりに使用することや、後述のラムダ式の引数の型に使用すること*5も可能である。
蛇足:autoに関しては、Objective-C++でも割と便利に使わせてもらっております。何しろ仕事でObjective-C++する場合、C++部分は実質的に「clangの『GNU拡張ありのC++』」な訳なので。
C++11以降:NULLではなくnullptrを使う
さよならNULL。君のことは忘れないよ。でも正直なところ、思わぬところでポインタではなく整数定数の0だと誤解されてしまう君には、少しうんざりしていたんだ。
const char *p = NULL;
const char *p = NULL;
const char *p = nullptr;
ポインタの代わりに参照を積極的に使う
C言語でポインタを使うケースの多くは、C++では参照に置き換えることが可能だ。
例えばクラス(構造体)のメンバを辿った奥底の値を使用したいが、長ったらしい名前を何度も書くのが面倒なので、一時的に別名を付ける場合:
std::string *value = &app.main_window.text_edit.value;
if (*value != old_edit_value) {
old_edit_value = *value;
do_foo(value);
do_bar(value);
}
このようなケースは、比較的簡単に参照に置き換えることが可能だ。
std::string& value = app.main_window.text_edit.value;
if (value != old_edit_value) {
old_edit_value = value;
do_foo(value);
do_bar(value);
}
ポインタを使うと、例えば引数にポインタをとる関数を実装するなら、プログラム中のどこかでNULLポインタチェックが欲しくなる。
static void func_foo(const struct STRUCT_FOO *foo)
{
if (foo == NULL) {
}
}
static void func_bar(struct STRUCT_FOO *foo)
{
assert(foo != NULL);
}
NULLチェックのような細々した処理が不要となる参照は、便利といえば便利だ。
static void func_foo(const STRUCT_FOO& foo)
{
}
もっとも、const参照の引数ならともかく、非const参照の引数の場合、その関数を呼び出す側にて例えば「foo(bar);」というコード片からbarが変更される可能性を予期しにくいという問題があって、参照を使うかポインタを使うか意見が割れるところだが……。
func_foo(baz);
func_bar(baz);
しかし冷静に考えると、それは構造体のポインタでも一緒だ。
func_foo(&baz);
func_bar(&baz);
要するに、通常のポインタを使っていて構造体のメンバを変更しているのか、constポインタでメンバを変更できないようにしているのか、呼び出す側のコード片からは判断できない。
というか、似たような(しかし全く異なる)問題は他のメジャーな言語でも見られる。例えばRubyの変数はポインタのようなもので、メソッドの引数は値渡しだ。なので、メソッド内で引数が参照しているオブジェクトを破壊的に操作すると、変化が波及する。メソッド内でオブジェクトを破壊的に操作しているか否かは、そのメソッドを呼び出す側のコード片からは判断できない。
foo(baz)
bar(baz)
まあ、C++の参照はポインタとは異なる代物なので、Rubyの例と同一視はできないのだが、しかし「メソッドを呼び出しているコード片からは、引数に設定した値が変化してしまうか否かが分からない」という点は同じだ。
そう考えると、深く考えずに非const参照の引数を使っても構わない気がしないでもない。
あとポインタを返す関数を参照を返すように単純に置換できるかというと、意外と「通常はオブジェクトを指し示すポインタだが、異常発生時にはNULLポインタを返す」という仕様の関数が多くて、一筋縄ではいかない。まあ、ポインタと参照は別物だからしかたない。この場合は「異常発生時にはNULLポインタを返す」という部分を「エラーコードを返す」や「例外をthrowする*6」などに仕様変更することになる。
キャスト演算にはC++のキャスト演算子を使用する
キャスト演算したい時、従来のC言語流のキャストではなくC++のキャスト演算子を使用した方がよい。コードの記述はやや冗長になるが、結果としてC言語流のキャストを使用した場合よりも安全なコードになりやすい。
static void func(uint8_t *data)
{
uint16_t n = (uint16_t) strlen((const char *) data);
}
static void func(std::uint8_t *data)
{
auto n = static_cast<std::uint16_t>(std::strlen(reinterpret_cast<const char *>(data)));
}
C言語流のキャストは複数の役割を担っており、少々複雑だ。そんな複雑な代物をよく理解せずに、むやみやたらに使用するのは危険だ。実のところ熟達したCプログラマは、キャストする時に「このキャストは、どのような役割のキャストか?」ということを暗黙のうちに判断しているものだ。
C++では、キャストの「複数の役割」を分解して、従来の役割から分岐した3つの演算と、C++固有の1つのキャスト演算を加えた、合計4つのキャスト演算子に整理している。
- const_cast
- dynamic_cast
- reinterpret_cast
- static_cast
ということで、C++のキャスト演算子を使用するという事は、強制的にプログラマに「このキャストは、どのような役割のキャストか?」と自問させて明記させるに等しい。考えさせる分だけ、結果としてコードが安全になる可能性が少しばかり上がるはずだ。
蛇足:なおObjective-C++においては、NSIntegerのようなプリミティブ型はC++のキャスト演算子でキャストできる一方で、Objective-Cのクラスのオブジェクトのアップキャスト/ダウンキャストにはC言語流のキャストを使用しなくてはならない。
名前の衝突回避に名前空間を使う(foo_func()ではなくfoo::func())
C言語でライブラリやモジュールを実装する際には、他のライブラリ等との名前の衝突を回避するために、名前のプレフィックスにライブラリ名を付けることが多い。
typedef struct {
int param1;
void *param2;
} FOO_START_PARAMS;
bool foo_initialize(void);
void foo_finalize(void);
bool foo_startBar(const FOO_START_PARAMS *params);
bool foo_stopBar(void);
せっかくなのでC++では名前空間を使う。
namespace foo {
struct START_PARAMS {
int param1;
void *param2;
START_PARAMS() : param1(0), param2(nullptr) {}
};
bool initialize();
void finalize();
bool startBar(const START_PARAMS& params);
bool stopBar();
}
念のため書いておくと、複数のコンテクストを扱いたいモジュール*7は、大抵は名前空間ではなくクラスで実装して、各コンテクストごとにオブジェクト化してしまうほうがよい。一方で、複数のコンテクストを扱うことなど到底ありえない場合*8は、他のオブジェクト指向プログラミング言語ならシングルトンないし「クラスメソッドonlyのクラス」を使うところだが、C++ではベタに名前空間を使っても許されると思う。だってC++なんだよ?
構造体やenumをtypedefしない
C言語では、例えばstruct FooをFooと書くためにtypedefしておく必要があった。
struct Foo {
};
typedef struct Foo Foo;
typedef struct {
} Bar;
static void func(void)
{
struct Foo foo1;
Foo foo2;
Bar bar;
}
一方でC++ではtypedefは不要だ。struct Fooを定義した時点で、structを付けずにFooと書くことができる。
struct Foo {
};
struct Bar {
};
static void func()
{
struct Foo foo1;
Foo foo2;
Bar bar;
}
全くの別名を付けたいのでもなければ、構造体やenumをtypedefする必要はない。
C++11以降:typedefよりもusingで別名を付ける
ところでC++11以降ではusingキーワードを使って型に別名を付けることができる。
typedef std::int32_t error_type;
using error_type = std::int32_t;
usingキーワードによる別名の付与には2種類ある。1つはエイリアステンプレートで、テンプレートのパラメータ(例えば「template <class T>
」のT)をそのまま含む型*9の別名を定義する機能だ。もう1つはエイリアス宣言で、従来のtypedefと同様にテンプレート以外の型の別名を定義する機能だ。
Better Cではエイリアス宣言の扱いが焦点となる。今まで通りtypedefするか、それともusingするのか? 後々テンプレートに手を出した時にエイリアステンプレートの記法と若干の統一性があることや、地味に関数ポインタの別名の書き方がスッキリしている点を考慮するに、typedefからusingに移行しても罰は当たらないだろう。
C++03以前:構造体の初期化にコンストラクタのメンバ初期化子を使う
C++の構造体(struct)は実質的にclassなので*10、メンバ関数を定義できるし派生(継承)も可能だ。もっとも、クラス/抽象データ型らしく使いたいならば、structではなくclassで定義した方が、コードを見ただけで意図が明確となる。structは、旧来のレコード型のようなケースで使うのが望ましい。なので個人的には、structに演算子オーバーロード以外のメンバ関数とか要らない気もするのだが……。
ただし、コンストラクタは構造体でも有用だ。メンバを初期化するコードを書いておけば、その構造体のオブジェクトをどこで定義しても、必ず同じ内容に初期化される。
struct Foo {
int a;
int b;
};
typedef struct Foo Foo;
static const Foo Foo_INIT_VALUE = { 0, ~0 };
static void foo_initialize(Foo *foo)
{
assert(foo != NULL);
foo->a = 0;
foo->b = ~0;
}
static void func(void)
{
Foo foo1, foo2;
foo1 = Foo_INIT_VALUE;
foo_initialize(&foo2);
}
struct Foo {
int a;
int b;
Foo : a(0), b(~0) {}
};
static void func()
{
Foo foo1, foo2;
}
C++11以降:構造体のメンバの初期化にクラス内初期化子を使う
ところでC++11以降では、非静的メンバ変数ならコンストラクタを使わずに初期化式にて初期化パラメータを記述することもできる。
struct Foo {
int a;
int b;
Foo : a(0), b(~0) {}
};
struct Foo {
int a { 0 };
int b { ~0 };
};
あるメンバ変数において、クラス内初期化子と「コンストラクタのメンバ初期化子」の両方が存在する場合は、メンバ初期化子の方が優先される。
個人的には、C++11以降では「既定値による初期化」にはクラス内初期化子を使用しておき、必要に応じてコンストラクタのメンバ初期化子を用いて「インスタンス生成時にユーザが既定値以外の値で初期化する」ことを可能にしておく、みたいな感じの使い分けがベターかなと思っている。
構造体の初期化にmemset(3)を使わない
そもそもC言語でも構造体をmemset(3)でゼロクリアする行為は割とダーティハックなのだけど*11、C++のstructは「デフォルトアクセスがpublicなclass」なので、C互換構造体ではない構造体をmemset(3)をするのは非常に危険だ。仮想関数テーブルなどの「ソースコードに書かれているメンバ変数」以外のオブジェクトを内包するインスタンスにたいして、memset(3)したら不味い領域まで「int型の0のビットの並び」で埋め尽くしてしまうことになる。
で、コードを書く際に一々「C互換構造体か否か」その他を確認してmemset(3)するのは非常に手間だし、「C互換構造体が後で非C互換構造体になったらどうしよう?」とか心配しだすと夜も眠れないので、もう最初からmemset(3)を使うのは諦めて、素直にコンストラクタなどを使うこと(ただしC言語で実装されたモジュール由来の構造体を除く)。
というか、もう一度繰り返すけど、そもそもC言語で構造体をmemset(3)でゼロクリアするのだって狂気と正気の境目でタップダンスを踊るのと同義な訳で(以下略)
蛇足:個人的には、C言語のコードを書く場合にも「構造体と対になる『構造体のメンバを明示的に初期化する関数』を用意して初期化に使用する」とか、面倒でもそういう風にするべきだと思う。
C++11以降:構造体を定義する際にfinal指定子を付与して派生(継承)禁止しておく
C++11にてfinal指定子が追加された。構造体(というかstructとclass)を定義する際に型名の後ろに付与することで、その構造体から派生(継承)することができなくなる。
struct Foo {
int a;
int b;
Foo : a(0), b(~0) {}
};
struct Foo final {
int a { 0 };
int b { ~0 };
};
※finalを書く場所に注意すること。C++のfinalは一般的なキーワード(予約語)ではなくコンテキスト依存キーワードである。所定の場所に記述した場合のみキーワードとしての効力を発揮するが、それ以外の場所では識別子として扱われる。
基本的に、Better Cで構造体を定義する際に「派生(継承)によるクラス階層の構築」を意識して設計・実装することは極めて稀である。つまり定義された構造体は、基底クラス(スーパークラス)として用いるには不適切な代物である可能性が高い。なので積極的にfinalを付与して派生(継承)を禁止しておく方がよいだろう。
同様に、Better Cからのステップアップでクラスを定義するようになった場合には、特定の課題の解決に特化したクラスを直接記述することが多く、派生(継承)を考慮した設計・実装になっていないことが多い。このような場合にも積極的にfinalを付与しておく方がよいだろう。
一方で「派生(継承)によるクラス階層の構築」を意識してクラス群を設計・実装する場合には、finalを控えめに使用するべきなのは言うまでもない。
構造体のオブジェクト同士で演算したいなら演算子のオーバーロードを使う
頻度は低いが、時々構造体のオブジェクト同士で演算を行いたいことがある。可能性としては、比較演算(特に一致・不一致)が多いだろう*12。
この時、専用の関数やメソッドとして演算処理を実装するのではなく、演算子のオーバーロードを使用しておくと、標準ライブラリのalgorithmや関数オブジェクトと組み合わせて処理を記述できる可能性が高くなり、後々の実装が楽になる……かもしれない。
struct Foo {
int param1;
int param2;
int param3;
};
typedef struct Foo Foo;
static void foo_initialize(Foo *foo)
{
assert(foo != NULL);
foo->param1 = 0;
foo->param2 = 0;
foo->param3 = 0;
}
static int foo_less_than(const Foo *lhs, const Foo *rhs)
{
assert((lhs != NULL) && (rhs != NULL));
return lhs->param1 < rhs->param1 ||
(!(lhs->param1 > rhs->param1) &&
(lhs->param2 < rhs->param2 ||
(!(lhs->param2 > rhs->param2) &&
lhs->param3 < rhs->param3)));
}
static int foo_greater_than(const Foo *lhs, const Foo *rhs)
{
return foo_less_than(rhs, lhs);
}
static int foo_equal_to(const Foo *lhs, const Foo *rhs)
{
assert((lhs != NULL) && (rhs != NULL));
return lhs->param1 == rhs->param1 &&
lhs->param2 == rhs->param2 &&
lhs->param3 == rhs->param3;
}
struct Foo {
int param1;
int param2;
int param3;
Foo() : param1(0), param2(0), param3(0) {}
~Foo() {}
bool operator < (const Foo& rhs) const {
return param1 < rhs.param1 ||
(!(param1 > rhs.param1) &&
(param2 < rhs.param2 ||
(!(param2 > rhs.param2) &&
param3 < rhs.param3)));
}
bool operator > (const Foo& rhs) const {
return rhs < *this;
}
bool operator == (const Foo& rhs) const {
return param1 == rhs.param1 &&
param2 == rhs.param2 &&
param3 == rhs.param3;
}
};
例えばstd::mapのkeyとして使いたいなら、比較演算子(デフォルトではoperator<
)が必須だ。
C++11以降:ホスト環境:構造体のオブジェクト同士で比較演算しそうな時に、構造体を止めてstd::pairやstd::tupleを使ってみる
std::pairやstd::tupleなら、デフォルトで比較演算子が定義されている。前提として、メンバ変数の型にて比較演算子が定義されている必要がある。
struct Foo {
int param1;
int param2;
int param3;
Foo() : param1(0), param2(0), param3(0) {}
~Foo() {}
bool operator < (const Foo& rhs) const {
return param1 < rhs.param1 ||
(!(param1 > rhs.param1) &&
(param2 < rhs.param2 ||
(!(param2 > rhs.param2) &&
param3 < rhs.param3)));
}
bool operator > (const Foo& rhs) const {
return rhs < *this;
}
bool operator == (const Foo& rhs) const {
return param1 == rhs.param1 &&
param2 == rhs.param2 &&
param3 == rhs.param3;
}
};
using Foo = std::tuple<int, int, int>;
構造体を単なるレコード型として用いる場合には、C++11以降の機能を使ってもよいのなら、std::tupleに切り替えるのも一つの手ではある。少なくともstd::mapとstd::tupleの相性は悪くない。
もっとも、構造体では自由なメンバ名を定義できるのにたいして、std::pairやstd::tupleには汎用な要素アクセス機能しかないため、既定の機能の範囲内では「格納している要素を含めた可読性」は構造体の方が向上させやすい。std::pairやstd::tupleの要素アクセスの可読性を向上させたいなら、typedefやusingで型に別名を付けた上で要素アクセス用関数を定義するか、std::pairやstd::tupleを基底とする派生クラスを定義してアクセサを用意するべきだろう。
using BaseFoo = std::tuple<int, int, int>;
struct Foo final : public BaseFoo {
Foo::Foo(int a, int b, int c) : BaseFoo(a, b, c) {}
int param1() const {
return std::get<0>(*this);
}
int param2() const {
return std::get<1>(*this);
}
int param3() const {
return std::get<2>(*this);
}
};
蛇足:「構造体とstd::tupleのどちらかを優先するべきか?」については、個人的には未だに明確な方針を得られていない(Kotlinでも「data classと『typealiasしたTriple』のどちらを使おうか?」と悩むことがある)。
おそらく公開インタフェースであったり、モジュール内で利用するデータ構造でも広範囲に使われるものについては、他者の可読性を考慮するに構造体か「『std::tuple + アクセサ』の一式」が望ましいだろう。
一方で、モジュール内で局所的に用いられる「一時的なデータセット」については、時に「わざわざ構造体を定義するよりも、std::tupleをそのまま(別名も付けずに)使ってしまった方が楽だ」というケースもある。
まあ、この辺は「自分がコードを書いている文化圏」の影響もあるからなあ……保守的なCプログラマが周囲に多いのならば、余計な摩擦を避けるために「なるべく構造体を使用する」という選択をするのも悪くない気がする。
ホスト環境:文字列は配列ではなくstd::stringやstd::stringstreamなどで取り扱う
文字列を保持する配列の大きさで悩んだり、ついうっかりstrncpy(3)の使い方を誤って終端のヌル文字が消えてバッファオーバーフローしたり、文字列の配列を引数にとる関数にてNULLポインタを引数に指定された際の対策を忘れて落ちたりするのは、何というか、非生産的だ。そうせざるをえない事情があるならともかく、普通にホスト環境でC++を使うシチュエーションで、そこまでシビアな条件である機会は少ないはずだ。
extern void foo(const char *src, char *dst, const size_t dst_size);
C言語ならともかくC++なのだから、std::stringやstd::stringstreamで幸せになりましょうよ……なれるかなあ。
extern void foo(const std::string& src, std::string& dst);
状況次第では、標準ライブラリにこだわる必要はなくて、CStringやQStringでも構わないのだが、少なくとも文字列専用の型を用いるべきだろう……特段の事情があるのなら話は別だが。
ホスト環境:配列よりもstd::vectorやstd::arrayを使う
用途次第だが、配列よりもstd::vector(C++11以降ならstd::arrayも)の方が幸せになれることも多い。
個人的にはsize()とend()の存在がありがたい。size()があると、普通の配列を使う場合のように要素数を求めるマクロ/テンプレートを別途用意しておいて使ったり、配列の要素数を示す定数を用意して使いまわしたりする必要がなくなる。end()があると末尾の次の要素を簡単に参照できるので、標準ライブラリのalgorithmやnumericの関数テンプレートと組み合わせやすくなる。
static const int TABLE[] = {
32, 64, 96, 128, 192, 256, 512, 1024
};
template <typename T, std::size_t N>
inline std::size_t NELEMS(const T (&)[N]) { return N; }
const int *p = std::find(TABLE, &TABLE[NELEMS(TABLE)], value);
if (p == &TABLE[NELEMS(TABLE)]) {
}
static const std::vector<int> TABLE {
32, 64, 96, 128, 192, 256, 512, 1024
};
std::vector<int>::const_iterator p =
std::find(TABLE.begin(), TABLE.end(), value);
if (p == TABLE.end()) {
}
あとC++11のinitializer_listは便利! 従来は、constなvectorを構築するために、vectorに設定する要素をまとめた配列を別途用意しておき、コンストラクタの引数に指定する必要があったが、その手間がなくなった。
template <typename T, std::size_t N>
inline std::size_t NELEMS(const T (&)[N]) { return N; }
static const int TABLE_SRC[] = {
32, 64, 96, 128, 192, 256, 512, 1024
};
static const std::vector<int>
TABLE(TABLE_SRC, &TABLE_SRC[NELEMS(TABLE_SRC)]);
std::vector<int>::const_iterator p =
std::find(TABLE.begin(), TABLE.end(), value);
if (p == TABLE.end()) {
}
static const std::vector<int> TABLE {
32, 64, 96, 128, 192, 256, 512, 1024
};
std::vector<int>::const_iterator p =
std::find(TABLE.begin(), TABLE.end(), value);
if (p == TABLE.end()) {
}
ところで、個人的な話になるが、std::arrayのようにサイズを指定できて、指定されたサイズを上限としてstd::vectorのように可変として振る舞うコンテナが欲しい。標準ライブラリに追加されないだろうか?
ホスト環境:コンテナ/std::bitset/std::pair/std::tupleなどを使う
C++の標準ライブラリには、std::vector以外にも多種多様なデータ構造のコンテナが含まれている。コンテナ以外にも、固定サイズのビット集合としてstd::bitsetが、構造体を定義するまでもない軽度な用途に使えるstd::pair/std::tupleが用意されている。
用途に合わせてお好きなデータ構造をお楽しみください。こういった類のものを自前実装したり適当なライブラリを探して組み込んだりしなくてもよい(標準ライブラリに用意されている)ことも、Better Cの利点の1つだと思う。
もっともコンテナに関しては、実行速度においてstd::vector一択となりやすい傾向にある。アルゴリズムの教科書的にはリストの方が高速となるケースでも、std::listよりもstd::vectorの方が高速なことがあるようだ。なので、ちゃんとプロファイリングするべし。
独自のデータ構造を定義する際にクラステンプレートの利用を検討する
機会は少ないものの、C++の標準ライブラリのコンテナではニーズに合わないために、独自のデータ構造が必要となることがある。
こういう時こそクラステンプレートの出番である、かもしれない……。
クラステンプレートは、その性質上、どうしても汎用なアルゴリズム/データ構造の実装に用いられることが多いため、プログラミングのレイヤーによっては縁が薄かったりして、存在を忘れていることも珍しくない。
アプリケーションを実装する場合、定義するデータ構造の多くは解こうとしている課題に特化したものであるが、時々「よく考えたら、より汎用なデータ構造を抽出できそうだ」ということがある。そういう時、汎用なデータ構造を定義する手段としてクラステンプレートを思い出していただければ幸いである。
固定長の独自データ構造を定義する際にクラステンプレートの利用を検討する
要するにstd::arrayっぽいアレである。
21世紀も5分の1が経過した現在でも、組み込みだけでなくスマホアプリやPCアプリの開発においても、あえてメモリの動的確保を回避することがある。例えばiOS向けオーディオアプリでは、オーディオアプリ開発でありがちな4つの間違いにてリアルタイム性を得るためのルールの1つとして「オーディオ処理用スレッドの中で動的メモリ割り当てをしないこと」が提唱されている。
そんなわけで、実は未だに固定長のリングバッファの類は重宝するのだが……こういうものこそ、std::arrayのように非型テンプレートパラメータを使用したクラステンプレートとして実装して、各所で地味に再利用したいものである。
蛇足:C++11でstd::arrayが登場したことにより誤解していたのだが、非型テンプレートパラメータの機能自体はC++98のころから存在するようだ。なので仮にC++03縛りの環境であっても、std::arrayのようなデータ構造をクラステンプレートで実装できるはずである……が、如何せん古いコンパイラで試したことがないので真偽は不明である。
――clangやgccに「-std=c++03
」を指定した状態でビルドと実行が可能なことは確認しているが、残念ながら「最近のコンパイラを旧規格のモードで使用した」だけだからなあ。
ホスト環境:値から値へのマッピング(写像)にstd::mapやstd::unordered_mapを使ってみる
C言語では、値から値へのマッピングに泥臭くswitch文を使う実装がよく見られる。
typedef enum { FOO_HOGE = 0, FOO_PIYO = 1, FOO_FUGA = 2, FOO_UNKNOWN = -1 } foo_t;
typedef enum { BAR_HOGE = 1, BAR_PIYO = 2, BAR_FUGA = 4, BAR_UNKNOWN = 0 } bar_t;
static bar_t bar_from_foo(const foo_t foo)
{
switch (foo) {
case FOO_HOGE: return BAR_HOGE;
case FOO_PIYO: return BAR_PIYO;
case FOO_FUGA: return BAR_FUGA;
case FOO_UNKNOWN:
default: return BAR_UNKNOWN;
}
}
少し工夫してデータ構造を用いる場合は、配列を使ったり(インデックス番号から、当該インデックスの要素にマッピング)、構造体の配列を使ったり(メンバAの値から、メンバBの値にマッピング)することになる。
typedef enum { FOO_HOGE = 0, FOO_PIYO = 1, FOO_FUGA = 2, FOO_UNKNOWN = -1 } foo_t;
typedef enum { BAR_HOGE = 1, BAR_PIYO = 2, BAR_FUGA = 4, BAR_UNKNOWN = 0 } bar_t;
static const bar_t MAPTABLE[] = {
BAR_HOGE, BAR_PIYO, BAR_FUGA
};
#define NELEMS(ary) (sizeof(ary) / sizeof((ary)[0]))
static bar_t bar_from_foo(const foo_t foo)
{
if (foo < 0 || (size_t) foo >= NELEMS(MAPTABLE)) {
return BAR_UNKNOWN;
}
return MAPTABLE[foo];
}
typedef enum { FOO_HOGE = 0, FOO_PIYO = 1, FOO_FUGA = 2, FOO_UNKNOWN = -1 } foo_t;
typedef enum { BAR_HOGE = 1, BAR_PIYO = 2, BAR_FUGA = 4, BAR_UNKNOWN = 0 } bar_t;
typedef struct {
foo_t foo;
bar_t bar;
} map_table_t;
static const map_table_t MAPTABLE[] = {
{ FOO_HOGE, BAR_HOGE },
{ FOO_PIYO, BAR_PIYO },
{ FOO_FUGA, BAR_FUGA },
};
#define NELEMS(ary) (sizeof(ary) / sizeof((ary)[0]))
static bar_t bar_from_foo(const foo_t foo)
{
size_t i;
for (i = 0; i < NELEMS(MAPTABLE); i++) {
if (MAPTABLE[i].foo == foo) {
return MAPTABLE[i].bar;
}
}
return BAR_UNKNOWN;
}
こういう用途にはstd::mapやstd::unordered_mapを使うことができる。
enum foo_t { FOO_HOGE = 0, FOO_PIYO = 1, FOO_FUGA = 2, FOO_UNKNOWN = -1 };
enum bar_t { BAR_HOGE = 1, BAR_PIYO = 2, BAR_FUGA = 4, BAR_UNKNOWN = 0 };
static const std::map<foo_t, bar_t> MAPTABLE {
{ FOO_HOGE, BAR_HOGE },
{ FOO_PIYO, BAR_PIYO },
{ FOO_FUGA, BAR_FUGA },
};
static bar_t bar_from_foo(const foo_t foo)
{
auto p = MAPTABLE.find(foo));
return (p == MAPTABLE.end()) ? BAR_UNKNOWN : p->second;
}
C++11のinitializer_listは便利っすね……。個人的には、この手のテーブルはソースコード上に予め記述しておくことが多いのだが、ごく稀に実行時に動的に構築したい場合もある。その場合は標準のコンテナを用いた方が手っ取り早かったりする。
ホスト環境:二次元配列による値から値へのマッピングをstd::pairとstd::mapに置き換えてみる
前項の亜種で、二次元配列のテーブルを使って整数値と整数値の組から値にマッピングするコードも、例えば std::map, T> のようなもので実現できる。
enum foo_t { FOO_HOGE, FOO_PIYO, FOO_FUGA, FOO_UNKNOWN };
enum bar_t { BAR_HOGE, BAR_PIYO, BAR_FUGA, BAR_UNKNOWN };
enum baz_t { BAZ_HOGE, BAZ_PIYO, BAZ_FUGA, BAZ_UNKNOWN };
static const std::map<std::pair<foo_t, bar_t>, baz_t> MAPTABLE {
#define PAIR(x, y) std::make_pair(x, y)
{ PAIR(FOO_HOGE, BAR_HOGE), BAZ_HOGE },
{ PAIR(FOO_PIYO, BAR_PIYO), BAZ_PIYO },
{ PAIR(FOO_FUGA, BAR_FUGA), BAZ_FUGA },
#undef PAIR
};
static baz_t map_flag(const foo_t foo, const bar_t bar)
{
auto p = MAPTABLE.find(PAIR(foo, bar));
return (p == MAPTABLE.end()) ? BAZ_UNKNOWN : p->second;
}
上記のようなケースでstd::unordered_mapを使いたいのなら、自前のハッシュ関数を実装する必要がある。それが面倒ならstd::mapを使った方がよいだろう。後でstd::mapがボトルネックになっていることが判明してから置き換えても罰は当たらないはずだ。
ホスト環境:配列やコンテナを舐める時にalgorithmやnumericを積極的に使う
標準ライブラリのコンテナを使っている際に、毎度毎度イテレータなどを使ってfor文で舐めているのは……色々な事情があってそうしているのならともかく、何も考えずに新規コードで常にそう書いているのを見ると「あまり近代的ではないな」と感じる。
static const std::vector<int> TABLE {
32, 64, 96, 128, 192, 256, 512, 1024
};
std::size_t i;
for (i = 0; i < TABLE.size(); i++) {
if (TABLE[i] == value) {
break;
}
}
if (i >= TABLE.size()) {
}
static const std::vector<int> TABLE {
32, 64, 96, 128, 192, 256, 512, 1024
};
std::vector<int>::const_iterator p;
for (p = TABLE.begin(); p != TABLE.end(); ++p) {
if (*p == value) {
break;
}
}
if (p == TABLE.end()) {
}
C++11以降の機能を使ってもよいのなら、Range-based for loopも悪くはない。悪くはないが、プリミティブなループ処理だ。
static const std::vector<int> TABLE {
32, 64, 96, 128, 192, 256, 512, 1024
};
auto found = false;
for (const auto& n : TABLE) {
if (n == value) {
found = true;
break;
}
}
if (!found) {
}
標準ライブラリのalgorithmやnumericの関数テンプレートでは、もう少し抽象的な機能を提供しているので、漁ってみる価値があるだろう。
static const std::vector<int> TABLE {
32, 64, 96, 128, 192, 256, 512, 1024
};
std::vector<int>::const_iterator p =
std::find(TABLE.begin(), TABLE.end(), value);
if (p == TABLE.end()) {
}
ちなみに、標準ライブラリのfunctionalには基本的な演算の関数オブジェクトが用意されていて、algorithmの関数テンプレートと組み合わせて使うことができる。構造体のオブジェクト同士で演算したいなら、演算子のオーバーロードで実装したり、構造体を止めてstd::pairやstd::tupleを使うようにしておけば、functionalやalgorithmの機能と組み合わせて処理を記述できるようになる。
C++03以前:関数ポインタよりは関数オブジェクトをやや優先する
algorithmに用意されている関数テンプレートには、関数ポインタを引数にとることが可能なものも多い。
static const int TABLE[] = {
1, 1, 2, 3, 5, 8, 13, 21, 34, 55
};
template <typename T, std::size_t N>
inline std::size_t NELEMS(const T (&)[N]) { return N; }
static bool even(int n)
{
return (n % 2) == 0;
}
std::size_t even_count = std::count_if(TABLE, &TABLE[NELEMS(TABLE)], even);
しかし実行効率の問題*13と、あと関数ポインタはNULLを突っ込むことができてしまうが「関数オブジェクト+参照」の組み合わせではありえないという点で、どちらかと言えば関数オブジェクト推しですな。
static const int TABLE[] = {
1, 1, 2, 3, 5, 8, 13, 21, 34, 55
};
template <typename T, std::size_t N>
inline std::size_t NELEMS(const T (&)[N]) { return N; }
struct Even {
bool operator()(int n) {
return (n % 2) == 0;
}
};
std::size_t even_count = std::count_if(TABLE, &TABLE[NELEMS(TABLE)], Even());
蛇足:まあでもC++11のラムダ式と比べれば、関数ポインタも関数オブジェクトも五十歩百歩な気がする。
C++11以降:ラムダ式(無名の関数オブジェクト)を積極的に使う
algorithmの関数テンプレートのお供にラムダ式。わざわざ関数/関数オブジェクトを別途定義する必要がなくなってうれしい。
static const std::vector<int> TABLE {
1, 1, 2, 3, 5, 8, 13, 21, 34, 55
};
auto even_count = std::count_if(TABLE.begin(), TABLE.end(), [](int n) {
return (n % 2) == 0;
});
うれしいのだが、書き方がObjective-C(というかApple独自拡張)のブロック構文と異なるのが……。Objective-C++にてObjective-CのクラスとC++のクラスが入り混じってカオスなところに、さらにブロック構文とラムダ式が追加されるだなんて、憂鬱だ。
NSArray<NSNumber *> *table = @[
@1, @1, @2, @3, @5, @8, @13, @21, @34, @55
];
auto indexes = [table indexesOfObjectsPassingTest:^(NSNumber *obj, NSUInteger idx, BOOL *stop) {
return (obj.intValue % 2) == 0;
}];
auto even_count = indexes.count;
蛇足:なんで急にObjective-C++を持ち出したのかというと、2022年現在においても、iOS/macOS向けにネイティブアプリを実装する際に局所的にお世話になることがあるからである……。
現時点において、iOS/macOS向けネイティブアプリを開発する際の第一言語はSwiftだ。それは間違いない。しかしながら、リアルタイム性が要求される機能を実装しようとした時、暗黙のうちにメモリ・アロケートや排他ロックが発生しうるSwift(そしてObjective-C)は都合が悪い。
そこで、アプリの大半はSwiftで実装しつつ、リアルタイム性が必要となる一部分のみをC言語やC++で実装することになる。C言語縛りだと実装が面倒になるので、(Better Cレベルだとしても)C++を使用することが多い。
問題は「C++で実装した機能をどうやってSwiftに組み込むか?」だ。SwiftはC言語との相互運用をサポートしているため、C言語の関数を呼び出すことが可能だ。でも残念ながらC++との相互運用はサポート対象外だ。
この場合のアプローチは2つある。
1つは「モジュールの公開インタフェースはC言語互換にしつつ、中身はC++で実装する」という方法だ。Swiftからは「C言語の関数を呼び出す」という使い方になる。正直なところ、この方法はSwift側からみて「モジュールの使い勝手がちょっと微妙」という印象を受けやすい。例えば……AppleのCore MIDI frameworkをSwiftから利用しようとして「面倒くさいぞ!」と思った人はいないだろうか? あの辺のAPI、未だにC言語のままなのだ*14。
もう1つは「モジュールの公開インタフェースは『Objective-Cのクラス』にしつつ、中身はObjective-C++で実装する」という方法だ。SwiftはObjective-Cとの相互運用もサポートしている。この方法で実装したモジュールは、Swiftからは「普通のクラス」として扱うことができるので、使い勝手は悪くない。ただしモジュールの実装は面倒だ。実装時にC++とObjective-Cをほぼ同時に扱うことになるし、SwiftとObjective-Cの相互運用に関するマナーにも通じておく必要がある。
――というわけで、「そこそこのリアルタイム性を確保する」というニッチで泥臭い要求に対応しつつ「モジュール利用者にもやさしく」ということを目指そうとすると、C++とObjective-CをちゃんぽんしたObjective-C++の世界となり、2種類のクラスと2種類のブロック構文と2種類のキャスト*15の競演で目が回るのである。悪酔いしそう。
関数テンプレートを使う(クラステンプレートは無理でも……)
型は違えどコードの見た目は瓜二つ、というコードを見かけた場合、関数テンプレートを使うことで、型を超えてコードを抽象化(一般化)できる可能性がある。
例えば値の範囲をチェックして、最小値や最大値から外れた値を範囲内にまるめたい場合、次のような関数テンプレートを用意しておけば、型に関係なく使いまわすことができる。
template <typename T>
const T& in_range(const T& val, const T& min_val, const T& max_val)
{
assert(min_val <= max_val);
return std::min(std::max(val, min_val), max_val);
}
あと、上記の関数テンプレートの場合、副次的に「全ての仮引数にて同じ型を強制する」という効果もある。コンパイラの種類や設定次第だが、例えばsignedとunsignedの比較は警告が出る程度で素通りしてしまうことが多い。だが関数テンプレートで同じ型を指定しているなら、型が異なる時点でコンパイルが通らない。否応なく「異なる型による比較演算」という事実を突きつけられたとき、プログラマがとりうる行動は「この比較演算は妥当か? どうすれば妥当になるか? キャストして問題ないか?」と見直しを図るか、「面倒だから(機械的に)キャスト!」と凶行に走るか、このどちらかだ。
個人的に、「クラステンプレートの実装」となるとちょっと身構えてしまう難易度のような気がしてしまう(錯覚かもしれない)が、関数テンプレートはもう少し易しい。オブジェクト・ファイルの大きさを気にしなくてよいのなら、使う価値がある機能だ。
C++11以降:自作関数が例外を送出するか否かチェックして、送出しないならnoexceptを付与する
C++11以降では、例外を送出しない関数にnoexceptを付与することで、パフォーマンスの向上を期待できる。
int triple(const int n) noexcept
{
return n * 3;
}
noexceptの付与は、パフォーマンス向上の他に「例外安全性(例外を送出しないこと)の保証」の役割も果たす。例外安全性を保証するためには、自分が書いたコードに例外が発生する余地が無いことを、注意深く検証する必要がある。
C言語には例外という機能は無いので、Better Cする時に「C++には例外があり、標準ライブラリで普通に使用されている」という事実を忘れがちである。なのであえてnoexceptを用いることで、例外安全性について検証する工程をコーディング時に設けた方が安全だろう。
C++11以降:overrideを積極的に使う
派生(継承)にてメンバ関数をオーバーライドする場合、overrideキーワードは非常に便利。特に、試作中で基底クラス(スーパークラス)のメソッドのシグネチャに度々変更が発生する場合とか(あまりよい開発スタイルではないのだが……)。
蛇足:しかしBetter Cなのにclassとか派生(継承)とか、レギュレーション違反では?
C++11以降:enum class(enum struct)を使う
従来のenumは名前の衝突が起きやすかった。そのため、例えば命名規則でプレフィックスを付けるなどして衝突を避けることが多かった。また型チェックが緩いため、整数型や他のenum型の変数に容易に変換できてしまった。
enum KeyCode {
KeyCode_A,
KeyCode_B,
KeyCode_C
};
enum KeyCode key_code_1 = KeyCode_A;
int key_code_2 = KeyCode_B;
C++03では、名前の衝突については、名前空間や構造体を用いることでも回避できた。しかし型チェックの問題は依然として残ったままだった。
namespace KeyCode {
enum Type {
A,
B,
C
};
};
KeyCode::Type key_code_1 = KeyCode::A;
int key_code_2 = KeyCode::B;
C++11では、scoped enumeration(enum classやenum struct)を用いることで、名前の衝突だけでなく型チェックの問題も回避できるようになった。型付けの制約から逃れたいならば、明示的にキャストを用いる必要がある。
enum class KeyCode {
A,
B,
C
};
KeyCode key_code_1 = KeyCode::A;
int key_code_2 = static_cast<int>(KeyCode::B);
scoped enumerationの強力な型付けは、enumで「関連のあるシンボル」の集合を定義して用るときにつまらない凡ミス*16を回避しやすくなる。
C++11以降:ホスト環境:文字列処理に正規表現を導入してみる
個人的には使用する機会は少ないが、正規表現が使えるようになった。検索や置換にて使用できないか、検討しても良いだろう。
蛇足:個人的に「スクリプト言語で正規表現を用いた検索・置換の記法」に慣れている身としては、C++では正規表現を用いた処理を直感的に書けないというか、むしろスクリプト言語では直感的に書ける反面予期せぬオーバーヘッドが発生していそう――と「C++で正規表現を使ったコード」から邪推してしまうというか、微妙な気分である。
C++11以降:ホスト環境:標準ライブラリの機能でマルチスレッドする
やっと移植先に応じてマルチスレッドのコードを書き分けなくても良くなった……。
なお古典的なスレッド・プログラミングに浸りきった人が見よう見まねでコードを書くと「std::thread・std::mutex::lock()・std::mutex::lock()unlock()・スレッド間でイベント通知するためのvolatileなフラグ変数」を多用しがちだが、処理の内容次第ではstd::threadを直接使用するよりもstd::async(std::launch::async)の方が扱いやすいことがあるし、std::mutexのアンロック忘れを回避しやすくするstd::lock_guardその他が存在するし、1回限りのスレッド間通信にはstd::promiseとstd::futureのセットがあるし、そもそも「1命令でのデータ書き換え」を期待してvolatileなプリミティブ型を使うのは止めてstd::atomicにしよう*17と声を大にして言いたい。
蛇足:個人的に、条件変数の使い道を理解しきれていない気がする――std::queueと組み合わせてスレッド間通信用のキュー(リアルタイムOSのメールボックスみたいなやつ)を作っただけで満足しちゃった。
RAIIによるリソース管理を享受する(例え提供する側にならなくとも……)
C++にはRAIIという「リソース管理に関するイディオム」がある。乱暴に言えば「コンストラクタでリソースを確保して、デストラクタでリソースを解放する」classを使用することで「変数の生存期間を利用して、自動的にリソースを解放させる」という方法だ。
これから新たにC++でBetter Cする人はほぼ確実にCプログラマだと思うので*18、Cプログラマを想定して書くと、C言語のコードのセマンティクスとしては:
- 内部変数は、定義した時点で生成されて、定義したブロックを抜けた時に消失する。
- ヒープ領域の変数は、malloc(3)やcalloc(3)で領域を確保した時点で生成されて、free(3)で解放した時点で消失する。
「何を急に当たり前のことを」とか「『消失した』後でもアドレスが分かっていれば残骸を参照できちゃうよね」とか突っ込み所は色々あると思うが、ちょっとだけ我慢してほしい。
C++でclassやstructを定義する際に、コンストラクタやデストラクタを追加することができる。ではコンストラクタやデストラクタはいつ実行されるか? ここで先に書いた内部変数とヒープ領域の変数の話が関わってくる。
- インスタンスが内部変数ならば、定義した時点でコンストラクタが実行されて、定義したブロックを抜ける時にデストラクタが実行される。
- インスタンスがヒープ領域の変数ならば、new演算子で生成した時点でコンストラクタが実行されて、delete演算子で解放する時点でデストラクタが実行される。
C++では「変数の生存期間」と「コンストラクタ/デストラクタの実行タイミング」が同期している。特にデストラクタの実行タイミングが明確である点は、ガベージコレクションを採用しているモダンな言語から見た時に特異にうつる部分だと思う*19。
RAIIを念頭に置いたclassでは、コンストラクタでリソースを確保して、デストラクタでリソースを解放する。なので、仮にRAIIに則したclassのインスタンスを内部変数として生成したならば、その変数を定義したブロックを抜ける際にデストラクタが実行されて、リソースが解放される。
RAIIは標準ライブラリで多用されているので、自分自身が提供する側にならずとも、その恩恵を享受できる。
例えばある関数内で、作業用のバッファを一時的に利用したいとする。必要なバッファサイズは実行時に動的に決まる。最大サイズについて明確な仕様が存在しないこともあり、伝統的な配列は使えない。C言語でホスト環境向けにクロスプラットフォームに書くならば「malloc(3)で確保して、使い終えたらfree(3)で解放する」という感じになるだろう*20。
#include <assert.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
static bool foo(const size_t buf_size)
{
assert(buf_size > 0);
uint8_t *buf = malloc(buf_size);
if (buf == NULL) {
return false;
}
free(buf);
return true;
}
この時、よくやりがちなミスが「作業中に問題が発生して途中returnさせた時にfree(3)し忘れていて、メモリリークが発生する」というものだ。
#include <assert.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
static bool foo(const size_t buf_size)
{
assert(buf_size > 0);
uint8_t *buf = malloc(buf_size);
if (buf == NULL) {
return false;
}
int rc = bar();
if (rc < 0) {
return false;
}
free(buf);
return true;
}
C++ではどうか? malloc(3)/free(3)のペアをnew/deleteに置き換えただけでは、同様の問題が発生する可能性がある。そこで標準ライブラリのstd::vectorを内部変数として利用する。std::vectorはRAIIに則しているので、関数を終了して内部変数が破棄されるタイミングでデストラクタが実行されて、内部で確保しているだろうヒープメモリが解放される。なのでメモリリークは発生しない。
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <vector>
namespace {
bool foo(const std::size_t buf_size)
{
assert(buf_size > 0);
std::vector<std::uint8_t> buf(buf_size);
auto rc = bar();
if (rc < 0) {
return false;
}
return true;
}
}
(std::vectorの生データ領域を配列代わりに使うことは、抽象化層の中に手を突っ込んでいるようで少々決まりが悪い。でもC++11でstd::vector::data()が追加されたから許されないかと考えている)
RAIIに関して個人的に興味深いのは、C11で追加されたstd::lock_guardだ。
C++のマルチスレッド機能には、排他制御用にstd::mutexが用意されている。排他したい区間の開始時にロックして、区間の終わりでアンロックする、という使い方をする。組み込みCプログラマであっても、リアルタイムOSを扱ったことがあるなら割と想像がつくと思う*21。
#include <mutex>
std::mutex sync_mutex;
{
sync_mutex.lock();
sync_mutex.unlock();
}
ここでよくあるミスが、途中returnなどの一部の経路にてアンロックを忘れてしまう、というやつだ。この場合、正常処理では何も起きないが、特定の条件で異常処理が走るケースに限ってデッドロックが発生する――という詳細が判明しない限り「時々システムがフリーズする(WDTアリなら『時々システムが再起動する』)」といういかにも面倒くさい不具合報告に悩まされることになる*22。
std::lock_guardは、RAIIを活用してミューテックスのアンロックし忘れを解消する機能だ。コンストラクタの引数でミューテックスを引き渡すと、コンストラクタの中でロックして、デストラクタにてアンロックする。
#include <mutex>
std::mutex sync_mutex;
{
std::lock_guard<std::mutex> lck(sync_mutex);
}
つまり途中returnだろうと何だろうと、スコープを抜けた時にアンロックされる。
特に排他区間のコード行数が長くなるケースや、排他区間内で「例外をthrowする可能性がある関数」を呼び出す可能性があるケースでは、デストラクタでミューテックスをアンロックするstd::lock_guardは非常に重宝する。
もしも組み込みでC++が利用可能ならば、リアルタイムOSのAPIのラッパーとしてRAIIを用いたクラスを用意して、リソース管理に使用したいところである。
蛇足:RAIIのような仕組みを簡易に実現できる言語機能がC言語にも欲しい。「Go言語のdefer」みたいな機能でもOK。リソース解放忘れ防止に役立つと思うのよ……リアルタイムOSのセマフォ/ミューテックス解放忘れでデッドロック、みたいな悪夢は嫌だ。
C++11以降:ホスト環境:どうしてもnewしたいならスマートポインタを併用する
個人的に、そもそも自前で動的メモリ確保するコード(C言語のmalloc(3)やC++のnew)を書くことが極端に少ないために、その影響でスマートポインタに全く習熟していない。
なので、スマートポインタについて言及するのはお門違いな気がするのだが……それでも、生ポインタに向かってnewするぐらいならば、まずはstd::unique_ptrを利用できないか検討した方が良いと思う(次点でstd::shared_ptr)。
C++のスマートポインタはRAIIに則している。std::unique_ptrの場合は、スマートポインタ自体が破棄されるタイミングで、ポイント先の「ヒープ領域に存在するだろうオブジェクト」を解放してくれる。
前項の作業用バッファを確保/解放する例で、RAIIを活用するためにstd::vectorを用いる旨を書いたが、同様のことはスマートポインタでも実現できる。
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <memory>
namespace {
bool foo(const std::size_t buf_size)
{
assert(buf_size > 0);
auto buf = std::unique_ptr<std::uint8_t[]>(new std::uint8_t[buf_size]);
auto rc = bar();
if (rc < 0) {
return false;
}
return true;
}
}
ただし、スマートポインタをバリバリ使用するためには、どこかの時点で右辺値参照やムーブセマンティクスについてある程度理解する必要がある。これって、CプログラマやC++03時代のプログラマからすると、微妙にハードルが高いかもしれない。
でも「ムーブによる所有権の移動」という考え方は学ぶ価値がある。先に書いたように「自前で動的メモリ確保するコードを書くことが極端に少ない」私であっても、リソース管理の観点で色々と考えた結果、std::unique_ptrを選択したことが1回、std::shared_ptrを選択したことが2回ある――というぐらいには、ホスト環境向けのコードを書く時に「タンスのいちばん下の引き出しにしまわれた道具」として出番がある。他の人ならば、もっと出番があるだろう。
まとめ
C++は難しくてよく分からない……まあ、私は所詮自称Cプログラマで、『Effective C++』すら読んでいないので*23、C++ガチ勢からすると妙なコードを書いていると思う。