WindowsではCtrl-cでPythonのスクリプトを即時中断できないことがある

CPythonの既知の不具合に遭遇した。処理系本体ではなく、標準ライブラリの問題のようだ。

界隈では有名な話かもしれないが、忘れないように個人的なメモを残しておく。

具体的には、標準ライブラリのsocketを使用して自前でUDPパケットを受信する、以下のようなスクリプトで遭遇した。

#!/usr/bin/env python3

import socket

BIND_ADDR = '127.0.0.1'
BIND_PORT = 41214

try:
    for ai in socket.getaddrinfo(BIND_ADDR, BIND_PORT, socket.AF_UNSPEC,
                                 socket.SOCK_DGRAM):
        family, socktype, protocol, _, sockaddr = ai
        with socket.socket(family, socktype, protocol) as sock:
            sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            sock.bind(sockaddr)

            print(f'listening on {sockaddr[0]}:{sockaddr[1]}')

            while True:
                data, addr = sock.recvfrom(65535)
                print(f'receive from {addr[0]}:{addr[1]} : {repr(data)}')
except KeyboardInterrupt:
    print('KeyboardInterrupt')

このスクリプトをコンソールで実行した時、macOS 11やUbuntu 20.04のPython 3.8と、Raspberry Pi OS bullseyeのPython 3.9では、Ctrl-cで終了した。しかしMicrosoft StoreからインストールしたPython 3.10 (amd64) では、Ctrl-cを入力しても即座には終了しなかった。

(1年前に、このスクリプトと同等の処理をCygwinPython 3.6で実行した際には、正しくCtrl-cで終了した記憶がある。なので、純粋なWindows版ビルドで発生する現象だと思う)

実験した感じでは、socket.recvfrom()ブロッキングしている最中にCtrl-cが入力された場合、入力された時点では何も起きない。Ctrl-c入力後、パケットを受信してsocket.recvfrom()によるブロッキングが解除された時点で、ようやくKeyboardInterruptが発生するようだ。

この挙動は既知の不具合のようで、GitHubのCPythonのリポジトリSIGINT blocked by socket operations like recv on Windows #85609というIssueがあった。Winsockの仕様(実装?)に起因する制約のようだ。*1

Winsock絡みということなので、今回はMicrosoft Store版でしか挙動を確認していないが、おそらく公式サイトで配布されているWindows版バイナリでも同様の症状が見られるのではないかと思う。

この問題の回避策の1つは、タイムアウトモードないし非ブロッキングモードのソケットを使うことだ。常にブロッキングさせるのではなく、例えば100ミリ秒から500ミリ秒ぐらいの短い周期でブロッキングが解除されるようにしておけば、解除された時にCtrl-cの入力に起因するKeyboardInterruptが発生して、スクリプトが停止する。

今回、問題に遭遇したスクリプトでは、お手軽なのはsocket.settimeout()を使用してタイムアウトモードを使う方法だった。

#!/usr/bin/env python3

import socket

BIND_ADDR = '127.0.0.1'
BIND_PORT = 41214

try:
    for ai in socket.getaddrinfo(BIND_ADDR, BIND_PORT, socket.AF_UNSPEC,
                                 socket.SOCK_DGRAM):
        family, socktype, protocol, _, sockaddr = ai
        with socket.socket(family, socktype, protocol) as sock:
            sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            sock.bind(sockaddr)

            # Workaround to handle keyboard interrupt on Windows.
            sock.settimeout(0.2)

            print(f'listening on {sockaddr[0]}:{sockaddr[1]}')

            while True:
                try:
                    data, addr = sock.recvfrom(65535)
                except socket.timeout:
                    continue
                print(f'receive from {addr[0]}:{addr[1]} : {repr(data)}')
except KeyboardInterrupt:
    print('KeyboardInterrupt')

もう1つの回避策は、いきなりsocket.recvfrom()TCPの場合はsocket.accept())を呼び出すのではなく、selectselectorsを使用して、ソケットが読み込み可能になるまで待ってから処理を行う、というものだ。

読み込み可能になるまで待つ周期を100ミリ秒から500ミリ秒ぐらいの短い時間にしておけば、タイムアウト後にCtrl-cに起因するKeyboardInterruptが発生して、スクリプトが停止する。

標準ライブラリのsocketserverではこの方法が採用されている。つまりserve_forever()の引数poll_intervalは、selectorsを使用してソケットが読み込み可能になるまで待つ周期だ。この引数に妙に大きな値を指定しなければ、Ctrl-c入力によるスクリプトの停止がいい感じに機能してくれる(引数poll_intervalのデフォルト値は0.5秒(500ミリ秒)だ)。

そういう意味でも、TCP/UDP通信のサーバ側を実装する際には、最初はsocketではなくsocketserverを使うことを検討しろ、ということなのかもしれない。

*1:このIssueには記載されていないが、WindowsにはPOSIXのシグナルがない(だからSIGINTなどの一部のシグナルのみをランタイム側でシミュレートしている)という事情も絡んでいるのかもしれない。

今までどのくらいプログラミング言語を触ってきたか(3秒で挫折したものものも含む) Ver.14

2022-06-19現在のステータス。
https://eel3.hatenablog.com/entry/2021/06/20/211327 から1年経て、こうなっている。

なおCSS、HTML、XMLはひとまず除外する。人工言語ではあるけれども「プログラミング言語」という括りに含められるか否かは議論が分かれる気がする。*1

よく使っている

AWK (Gawk)
単純なテキストレコードの処理はAWKで十分間に合う。今の時代、自作ツールをnawkやGNU awk単体で実装するのは苦行すぎて*2皆無なものの、シェルスクリプトMakefileAWKのコードを埋め込むなどして他のコマンドと組み合わせて使う機会は依然として多い。シェル上でワンライナーでテキスト処理する時にも重宝している。これはこれで十分AWKらしい使い方ではないだろうか?
C++
ちょくちょくお仕事で使うが、未だに本職のC++使いではない。C++11やC++14は非常に便利で、better Cでも使う価値がある。C言語使いからすると、C++03時代よりも充実度が進んだ標準ライブラリ、ラムダ式、autoによる型推論――などなど、便利で羨ましい限りだ*3。あと、Swift時代のクロスプラットフォームC++ライブラリ作者は、どうあがいてもARCから逃れられないので、C++11以降のスマートポインタは必須だ*4正規表現とスレッドも標準ライブラリに加わったので、あとはソケット(低水準ネットワークAPI)をサポートしてくれないだろうか。C++17のstd::optionalも、KotlinやSwiftの経験者としては好ましい。低水準の処理を行いつつも便利な機能で実装時間を短縮できる点は便利で、少なくともシステムプログラム言語としての利点だと思う。だけど機能多すぎ/複雑すぎなところはなんとかならないものか。強力な反面、使い手を選ぶ言語だ。
C言語
お仕事での主力言語だった――ここ最近は使ってないなあ(C++のコードの一部にC言語寄りのコードを埋め込むことはあるけど)。シンプルかつ低水準の世界が垣間見れるところが割と好き。とはいえ最近の他の言語と比較すると、シンプルすぎて安全機構が欠けていたり標準の便利機能が少なかったりするので、入門用の言語としては薦められない。にもかかわらず、かつてはプログラミング未経験者向けのC言語の本が盛んに出版されていた――あれ、何だったのだろうか? 謎だ。クロスプラットフォームなモジュール屋としては、今までC89を採用してきたものの、いい加減そろそろC99とかC11とか次世代の言語とか使いたい。でも「まだサポート終了していないVisual Studio」のことまで考えると、C99すら2023年まで使えない*5。悲しいなぁ。
DOSバッチファイル
プログラミング言語に含まれるかどうか不明だが、含めてしまう。ちょっとした自動化や、複数ツールを組み合わせて使うときのラッパーとして、今でもよく使う。コマンドプロンプトはシバン(shebang)に対応していないので、スクリプト言語で書いたツールを起動するラッパーとしても多用している。意外と色々なコマンドが用意されているので、単純にそれらを叩く分には十分だが――言語機能がショボいので、バッチファイルでifやforのような制御構文系コマンドが必要になってきたら、何か決定的に間違えていないか、考え直すようにしている。
JavaScript(クライアントサイド)
ものすごく久しぶりにクライアントサイドJavaScriptのコード触った――いまどき珍しい、DOM直叩きスタイルだけど。収穫は、最近のECMAScriptのスタイルに触れたぐらいだろうか? フレームワークもTypeScriptも触っていないので、クライアントサイド開発のスキルは依然として賞味期限切れのままだ。
JavaScript(サーバサイド?)
初めてお仕事でNode.js向けのJavaScriptのコードを触った。Web開発の外の人からみた印象としては、ブラウザ以外のJavaScript処理系はNode.jsに収斂しちゃった感があるなあ――Denoもあるけど、エコシステム的に今後どうなるんだろう?
Kotlin
本格的にAndroidアプリ開発に関わるようになったのがGoogle I/O 2017直後の過渡期なので、JavaよりもKotlinでの経験値の方が多い。モダンな「強い静的型付け」の、割とええ感じの言語やね。ただ、使い始めが「Swift 3をつまみ食いして半年以上経ってからKotlinをつまみ食いした」みたいな経緯だったこともあり、未だに両者の概念・機能が頭の中でごった煮になっている。それと、NDK絡みの作業が多いので、C++11/14・Java 7/8・Kotlinを行ったり来たり。泣けるぜ。Swiftもそうだが、最近のメジャーな「強い静的型付け」の言語は「開発環境込み」で高い生産性とコードの安全性を両立させる方向に進んでいる気がする。
make (Makefile)
プログラミング言語に含まれるかどうか不明だが、DSL扱いで……いやGNU Makeはそこそこプログラミング言語的か。GNU Make 4.0はさらにプログラミング言語的だな、特にGNU Guileなところが。GNU MakeとNMAKEが主力で、稀にNetBSD Make(pmake)を使うが、いずれも独自拡張アリアリだ。もう素のmakeではMakefileを書けない :)
Objective-C, Objective-C++
時代はSwiftだと言われて久しいけど、どっこいObjective-CObjective-C++は生きている。というかSwiftのコードにC++で書かれたライブラリを直接組み込むことができない以上、両者を繋げるグルー言語として生き残ることになるよね。一定以上のリアルタイム性が求められるアプリをSwiftだけで書くのは厳しくて、どうしても部分的にC言語C++を使うことになり、グルー言語としてObjective-Cが召喚されることになる。最近流行の言語と比べると良くも悪くも80年代的だが、アプリケーションプログラミング用としてはC言語よりマシだし、C++ほど複雑怪奇*6ではない。そしてC言語C++で書かれた既存のライブラリをそのまま使える。Objective-Cのハイブリッドな所は好きだが、Objective-C++はハイブリッドすぎて――C++のクラスとObjective-Cのクラスを、C++ラムダ式Objective-Cのブロック構文を同時に使うのは大変だ。便利ではあるんだけどね。
Python
Python 2.xで実装した小ツール群をPython 3.xに移植した。Pythonではlazyなスタイルでのコーディングが許されず、整然とコードを記述する必要がある。その辺は、Perl 5やRubyとは随分と雰囲気が異なる。気になるのは、インデントが必須な言語仕様であるために、シェルスクリプトに埋め込んで使うのが苦痛な点だ。Pythonだけでコードを書く分には気にならないのだけど。
Swift
コンパイラによる強力な型推論と型安全性のチェック」がお仕事用のメジャーな言語にまで降りてきたという点で、Swiftは静的型付け言語界のJavaScript*7だと思っている。でもユーザ数的には、Kotlinが「静的型付け言語界のJavaScript」ポジションなのかもしれない。割と好感が持てる言語だが、知識が中途半端にKotlinとごった煮になっているので、ついうっかりif式を書こうとしてコンパイルエラーになったり、「varval」と「varlet」の振る舞いの差異につまづいたりしてしまう*8
シェルスクリプト (/bin/sh)
プログラミング言語に含まれるかどうか不明だが……いや、私的にはシェルスクリプトは立派なプログラミング言語だ。基本的な用途は、バッチファイルと同じくちょっとした自動化や複数コマンドを組み合わせて使うときのラッパーだが、実現できる内容は遥かに多い。言語本体(?)がバッチファイルよりも高機能だし、Unixユーザランドはコマンドが充実している。その意味では、WindowsではMSYSよりもCygwinで――いやむしろWSL(Windows Subsystem for Linux)で環境構築すべきだろう。Cygwinでは、主要な処理をシェルスクリプトで記述しておき、bashからはシェルスクリプトを利用し、コマンドプロンプトではラッパーのバッチファイル経由でシェルスクリプトを叩く使い方をしている。ただWindows上では処理速度が妙に遅くなる点が不満だ。まあしかし、Unixのシェルは言語設計もシステム開発技法も未成熟だった大昔に「プアな環境でも問題なく動作する、プログラマブルな対話型コマンドインタプリタ」として開発された代物なので、言語設計の研究が進んでから作られたプログラミング言語と比較してはならない。

あまり使っていない

Perl 5
時々、やむをえない事情で触ることがある。だが基本的によく分からない。何というか、あの記号の羅列っぽさに中々慣れないというか、自分は余りに自由度が高すぎる言語は苦手だと気づいたというか。(言語仕様に慣れているなら)半ば使い捨てなテキストフィルタとかをさっと書くに分には悪くない言語だと思うのだけど。
Ruby
自作ツール実装にて、AWKの代替言語の最有力候補だった*9。テキスト処理でも割と重宝するが、バイナリデータへの変換が絡んでくるとAWKよりもRubyを使った方が効果的だ*10。そろそろirbを電卓代わりに使うスタイルも板に付いてきた気がする。to_s(16)やto_s(2)で基数変換して表示できるところが好き。
sed
プログラミング言語に含まれるかどうか不明だが、DSL扱いで*11。テキスト処理用。シェルスクリプトMakefileにて他のコマンドと組み合わせて使う。というか正規表現でのテキスト置換以外の機能を使った記憶が……あったな、dとiとpと=とブレースによるグループ化ぐらいだが。私の技術レベルではsedFizzBuzzを書けないので、sedで難しい処理を記述しないようにしている。
Windows PowerShell
時代はPowerShell Coreらしいが、現行のWindows 10でデフォルトで利用できるv5.1に留まったままである。スクリプト言語としてのPowerShellは、オブジェクト指向.NET Frameworkを叩けてダイナミックスコープでスクリプトブロック(という名の無名関数)と、無茶でピーキーで完全にプログラマ向けな代物だ。Microsoftもよくもこんなエライ代物を出したものだ。残念なことに、コマンドプロンプトの代替という観点では、外部ツールとの親和性が微妙にイマイチだ(特に文字コードとか)。でもPowerShell内で閉じている分には問題ないので、私の手元では「Windows専用のGUI付き小ツールを作るためのスクリプト言語」か「Excel COMとか叩く用のスクリプト言語」か「Windows Serverの管理スクリプトを書くためのスクリプト言語」扱いしている。ところで、いい加減『Windows PowerShell イン アクション』並みの言語解説書の最新バージョン対応版を出版してくれないだろうか。

最近使ってないが、縁は切れてない

bash
最近はデフォルトシェルがbashな環境も多いので、自分用のツールぐらいは素の/bin/shではなくbashで書いても大丈夫な気がしてきた。shよりbashの方が遥かに便利だからなあ――PerlRuby等には負けるけど。bashスクリプトを書くときの唯一の欠点は、メジャーバージョンごとの差異や各ディストリでのビルドオプションの違いにより、同じbashという名前でも実は千差万別なところだと思う。PerlRubyのバージョンは気にするけど、これがシェルになると意外とバージョンに無頓着になってしまう。なんでだろう?
C#
かつて、勉強を兼ねてC# 2.0を少し触ろうとするも未完に終わり、数年後にあらためてVisual Studio 2013をインストールして少しだけ触った*12けどほんの少しだけで終わった過去をもつ私。変数の型推論ラムダ式LINQ・デフォルト引数は便利だなあと思っていたら、いつの間にかC# 8.0になってKotlinやSwiftに見られる流行を取り入れてますな。おっちゃん、付いてくのが大変だよ。.NET Frameworkの機能数は反則ものだが、所々に微妙に抽象化が行き過ぎたAPIが見られるのは気のせいだろうか? それにしても、クラスが必須ではないC言語C++に慣れてしまった弊害か、アプリケーション・メインエントリすらclass内に定義しなくてはならないC#には、なかなか慣れない。
Free Pascal
お試しで触っているのだが、微妙にDelphi/Free Pascal初心者(ただし他言語の経験者)向けの良い資料が少なくて難儀している。玉石混交なのだ。いっそのこと『OBJECT PASCAL HANDBOOK―マルチデバイス開発ツールDelphiのためのプログラミング言語完全ガイド』を買ってしまおうかしら……と思っていたら絶版っぽい。
Go
寡作ながらもいくつか小ツールを書いてみたが、標準ライブラリが充実しているコンパイラ型言語っていいっすね。C言語に比べればC++の標準ライブラリも充実しているが、どちらかといえばプリミティブな機能が中心だ。PythonRubyばりの標準ライブラリを抱えているGoには及ばない。その辺は、やはりCプログラマ(特にCでフィルタやデーモンの類を書く層)には受けそうな言語だと思う。並列処理周り(goroutines)とかARM対応とかが気になる。ソフトリアルタイム限定だが「組み込みLinux + Goで書いたデーモン」とかどうだろう? ただメモリを食うらしいという噂がどうなったか気になる――64bit環境では解消されるという話だったようだが、32bit環境でも解消されるようになったのだろうか? 組み込みでは現時点では逆立ちしたって64bit CPUはありえないからなあ、スマホタブレット以外では。
Java
生まれて初めて触れたプログラミング言語その2。実のところ、職業プログラマとして本格的に使用することは一生ないと思っていた。Androidアプリ開発も、Kotlin採用後に本腰入れて関わるようになったので、Kotlinメインだ。だが、なぜかぬるい感じに時々Javaのコードを触っている。先にコレクションの操作方法が充実した他の言語を学んでからJavaを本格的に触るようになったので、Java 8以降のStream APIが使えないと身体が拒否反応を示す。少なくとも、構文の見た目こそ保守的なオブジェクト指向プログラミング・スタイルで書かれたC++に似ているけど、中身はC++とは似ても似つかない代物だということは体感している。
Lua
Wiresharkのパケット解析スクリプトを書いたことも、C言語で書かれたUnixデーモンの設定ファイル用に処理系を組み込んだこともあった*13。あれから数年経ったが、今はどんな感じなんだろう?
QML
宣伝文句のとおり、QMLはGUIの記述に非常に向いている。それも、単に標準のUI部品(エレメント)を使うだけでなく、少し改造して使うとか、オリジナルのUI部品を作って使うとか、それらを別のアプリケーションに使いまわすとか、そういう時に威力を発揮する。あと、プロパティバインディングやレイアウトのアンカー指定など、画面サイズの変更に追随するUIを作りやすい機能も揃っている。JavaScriptでちょっとした処理も記述できる――とはいえ、やりすぎるとパフォーマンスの罠が……。少なくとも、JavaScriptでゴリゴリコードを書くのはQML的ではない。QMLは宣言的に、シンプルに書くものだ。力技でロジックでゴリ押しすると、色々と罠に嵌る言語だ。
Scheme
GaucheWindowsネイティブ環境用バイナリは実験版だが、私が触る分には何の支障もない*14ことに気づいて久しい今日この頃。『Scheme手習い』と『Scheme修行』を購入したので、とりあえずCommon LispではなくGaucheScheme)の勉強をする方向に転換しようか検討*15しているうちに何年たったのやら。Gaucheはフィルタ・ライクな小ツールの実装用としても良い感じだ。しかし最も多い利用方法はREPLを電卓代わりにすることだ*16。うーん、作業環境がmacOSLinuxに移ったなら、大手を振ってGaucheでフィルタを書くのだが。
SQL
生まれて初めて触れたプログラミング言語その3ぐらいに位置する。組み込みの人なのでSQLとは無縁だと思っていたが、まさかTransact-SQLを少しだけ触ることになるとは。最近はAndroidアプリ絡みでSQLiteに触れることもあるが、AndroidXのRoom経由だったり、ContentResolverのqueryだったりと、フルセットのSQL文ではなく局所局所でDSL的に使う感じである。
Tcl/Tk
Tclは書き方を忘れた頃にテキスト処理ツールを書いている気がする。Tclは結構独特な言語だ。構文がシェルスクリプトばりに全てコマンドだったり、値が全て文字列だったり、実はリスト構造だったり、意外とTCPソケット通信が得意だったり……。それでも慣れれば結構使いやすい。意外とプロトタイピングに向いている気がする。8.6以降ではオブジェクト指向プログラミングもOKだが、それよりも例外処理用のtry末尾呼び出しの最適化用のtailcallの方が興味深い。しかし、これからメジャーになる可能性は低そうだ。Tkは……小規模なGUIツールをさくっと構築できるところは便利だが、Webアプリ全盛の時代にどれだけ訴求力があるのやら。
Visual Basic .NET
Visual Basic .NET 2003で書かれたコードを時々メンテ中。流石に開発環境はVisual Studio 2013移行したけど。
XSLT
よく考えてみたら生まれて初めて触れたプログラミング言語その4ぐらいに位置する言語だった。縁が切れたと思いきや、仕事でXHTMLから特定要素を抜き出す作業に使うことがあったり……。XMLからテキストレコードに変換してしまえば、後はUnix流テキストフィルタの世界が待っている。餅は餅屋というもので、定型的なXMLの変換はXSLTで記述するべきか。唯一気に入らないのは、xsl:sortでアルファベットの大文字と小文字を区別してソートすることができないこと。ぐぬぬぬ。

これから(また)使うかもしれない

Alloy
形式手法の中では比較的カジュアルに使えそうなので期待中。入門書も処理系も入手した。私の場合、先に何か論理型の言語をかじった方がよいのかも。
bison (yacc)
プログラミング言語に含まれるかどうか不明だが、DSL扱いで。やっぱり構文解析系統のコードを自作するのは割に合わない――だなんてうそぶきつつ、LALR法とか全く知らないままに、既存のyaccのコードを切り貼りして遊んでみた。簡易電卓レベルだが便利さを体感しつつ、さっそくtypo 1文字で痛い目(shift/reduce)に遭った。とりあえず、flexと組み合わせた上でのエラー処理(エラーメッセージの改善)が課題だ。
Common Lisp
2009年に勉強しようと思い立ったものの、未だに進んでいない。階乗とかハノイの塔とかiotaぐらいは書いたが、目標は「ちょっとしたツールを自作する」だ。まだ道は遠い。最近は時々CLISPを簡易電卓代わりにしている。
Coq
ソフトウェアの基礎が気になるので、処理系だけ入手。
F#
OCamlは「Windows上で日本語を扱う」という視点では処理系がちょっと微妙なので、いっそのことF#に乗り換えようかと……。『実践F#』が積読状態になっている。
flex (lex)
プログラミング言語に含まれるかどうか不明だが、DSL扱いで。字句解析用のツールという印象が強かったのだが、よく考えてみたら、flexは「sed(1)のよくある使い方」と同様に「正規表現でパターンマッチング --> 何らかのアクション」という内容を記述するためのツールだった。ただ単に、「何らかのアクション」をC言語で書けることと、flex自体ではパターンマッチングをせずに「パターンマッチングするC言語のコード」を生成することが少々風変わりなだけ。grep(1)やsed(1)その他で小ツールを実装して運用しつつ、性能が求められたらflexで専用ツール化する――とか考えたけど、普通にgrep(1)やsed(1)を使う方が高速だった。
Forth
pForthをMinGWでビルドしたので処理系は手元にある。スタック指向の言語はいつか勉強したい。
Io
プロトタイプベースである点を除けば、何となくSmalltalk的であるような――公式ドキュメントらしきIo Programming Guideでも影響を受けた言語として真っ先にSmalltalkが挙げられているから、あながち思い違いでもないだろう。今更ながら『7つの言語 7つの世界』のIoの章を読み終えたので、ちょっとしたコード片を書いているが……Windows版のバイナリが古いためか、リファレンス通りなのに動作しないコードに直面している。
LOGO
そういえばLOGOを触ったことがない。とりあえずUCBLogo(Berkeley Logo)だろうか? Windows上でUCBLogoばりにGUI無しで動作する処理系はないだろうか?
Object REXX
思うところがあって処理系とIBM謹製のドキュメントを入手したものの、そこから先の進展は無いまま。ReginaでClassic REXXっぽい感じで触っているからなあ。
OCaml
Common Lispを勉強するはずが、いつの間にか触っていた言語。一応、階乗ぐらいは書いた。時間が取れたらもうちょっとしっかりと勉強したいが、面倒なのでF#に移行しようか検討中。
Oz
ふと思い立ってUbuntuにMozartを入れた。『Scheme手習い』の次はCTMCP片手にOzで勉強かなあ。道は遠いな……。
PostScript
これかForthか、どちらに手を出すべきか? 悩ましい。
Processing
入門書も処理系も入手して、あとは弄る時間をつくるだけ。
Prolog
『7つの言語、7つの世界』の地図の色分けプログラムには衝撃を受けた。何というか「正しい問い」を見つけられるか否かが肝なのか。この辺は、根底の部分でAlloyに通じる何かがあるように思う。ひとまず、Prologで論理プログラミングと宣言的なスタイルに慣れておけば、形式手法にて「論理で宣言的に」記述するときに戸惑いが減るのではないかと期待している。
Rust
仕事柄「C/C++の次のシステムプログラミング言語」はそれなりに興味の対象で、Go言語やD言語ほどではないが、Rustも……まあ、気にならなくはない。ちなみに、これら3言語と同列にObjective-CやSwiftが挙げられることもあるようだが、個人的見解としては、システムプログラミング言語としてのこの2言語には全く期待していない。あれは、Appleというしがらみからは逃れられないでしょうな。
VBA (Visual Basic for Applications)
今までVBAから逃げ回っていたのだが、ついに使うことになりそうな予感。たぶん、Access VBA 8割にExcel VBA 2割ぐらい。

今は全く使っていない

Active Basic
VBScripを触りだした影響で、時々思い出しては弄くっていた。ほんの少しだけ触って放置して、すっかり忘れてからまた触る――これを繰り返していた気がする。なので毎度初めて触るのと同じ状態だった。String型をバシバシ使用 :)
bc
その昔、Windows標準の電卓アプリの代わりに使おうとして色々あって挫折した。今はirbclisp/goshで計算しているからなあ。
CASL II
生まれて初めて触れたプログラミング言語その1。何だかんだで、後でCプログラマになってからも低水準での思考ツールとして微妙に役に立っている。まあ考えるための言語であって実用言語ではない。仮に実用的な処理系*17があったとしても余りに命令がシンプル過ぎて悶絶するなあ、なんてFizzBuzzしてみて思った。
Clojure, Scala
JDKがなくてもJava APIを叩くスクリプトを書けるので非常に便利。Scala型推論とか、便利っすね。言語仕様はJavaよりも好みだ。とはいえ、IoT時代にJava VMベースでどこまでメインストリームに居残ることができるのか? ちょっと興味深い。サーバサイドに活路を見出すのだろうか?
COBOL
FizzBuzzするためだけにOpenCOBOL 1.0をWindows上に用意して触ってみた。なんというか、COBOLの名前と生まれた時代が示すように基幹業務(というかお金や帳簿が絡んでくるところ)向けの言語だよなあ、といった感じ。COBOL 2002のフリーフォーマットを採用するだけでも使い勝手が変わる気がしたが、世の中にはまだ広まらないのだろうか。
CoffeeScript
仕事で使う予定はない。RubyPythonその他の影響を受けているだけあり、その手のスクリプト言語っぽい感じでコードを書けるので、慣れれば素のJavaScriptで直接コーディングするよりは楽だ。しかし標準ライブラリ回りや処理系絡みの機能やサードパーティのライブラリなど、結局はJavaScriptを知らないとCoffeeScriptでコードを書けないと思う。それに生成されたJavaScriptのコードを見て「うわぁ、これあまり効率的でないなあ」と感じる時もあって、高速化が必要な部分では生成されるコードを気にしながら記述したりCoffeeScriptを諦めてJavaScriptで書くことになるので、やはりJavaScriptを知らないとマズイ。とはいえ便利なのは確かだ。CoffeeScriptのコードは即Node.jsで実行できるので、その辺りから「CoffeeScriptでテキストフィルタ」的な文化が生まれると面白いかも。気になるのはECMAScript 6の存在で、今までCoffeeScript独自の機能だった部分の多くがES6で取り込まれるので、今後ES6対応が進むにつれてCoffeeScriptの立場がどうなっていくのか、少々興味深い。
D言語 2.x
仕事柄「C/C++の次のシステムプログラミング言語」はそれなりに興味の対象で、Go言語ほどではないが、D言語も気になる存在だ。D言語シンタックスがC・C++に近いだけでなく、コーディングしている時のアプローチ・判断についても、CやC++での流儀がそこそこ通用しやすい気がする。少なくとも、Go言語でコーディングするよりは、文化的背景の違いによるモヤモヤは感じにくい。あと、標準ライブラリを使ってテキストフィルタを書いたところ、エラー処理を1~2ヶ所のtry - catchにスッキリまとめることができて、ちょっと驚いた。throwされる例外のメッセージ文字列が、ちょうどよい塩梅の内容だったため、メッセージを変更する(いったんcatchして、再throwする)必要がなかった。ちょっと残念なのは、マルチバイト対応だが……。
Emacs Lisp
.emacsにコピペ」限定で。Common LispSchemeを触ったためか、何となく内容を追えるようになってきた気がしていたが、勘違いだった。
Fortran
Fortran 90やFortran 95あたりは結構近代的な言語だと思う。用途次第ではC言語よりもFortranの方が遥かにマシな選択だろう。配列がらみの処理はFortranの方が得意だし、言語機能としてのモジュール化の方法はC言語には存在しない。可変長な文字列の扱いに微妙な制限がある点はマイナスな気もするが、まあ基本的に数値計算プログラム用の言語だからなあ。
GDB (GNU Debugger)
……いやGDBはデバッガとして使っているが、GDBスクリプトを書く機会は(FizzBuzz以外に)ない。勉強不足なだけかもしれない。
Groovy
JDKがなくてもJava APIを叩くスクリプトを書けるので非常に便利。動的型付け言語っぽくいくもよし、@CompileStaticや@TypeCheckedで型推論するもよし。言語仕様はJavaよりも好みだ。コンソールアプリを書く人としては、オプション引数解析用の機能を標準で持っている点で、GroovyはClojureScalaよりもポイントが高い*18。個人的には、IoT時代に「Java VMベース」の言語としてどこに活路を見出すのが、興味深く見守りたいところ。やはりサーバサイドだろうか?
HSP (Hot Soup Processor)
FizzBuzzで楽しんでみたが、何というか他言語経験者には受けが悪そうな命令体系だと思う。もっとも初心者がプログラミングという行為に深入りせずにWindows用のGUIな何かを作る分には、あの命令体系でも十分な気がしないでもない。ところで元々は「HSPで職業プログラマ的な良いコードを書くと、どんな感じになるか?」というネタを思いついて処理系を用意したのだけど、そちらは全く進展がないまま。
JScript on WSH
他人が使うテキスト処理ツールの実装に使って以来、時々触ってきた。Windows用の配布可能な小ツールを実装する時の定番言語だった。でもそろそろ潮時だろう。HTAと組み合わせてクライアントサイドJavaScriptなノリで簡易なGUIツールを実装できる点も、PowerShell + WPF + XAMLで代替できそうだ。他のメリットは「JavaScriptECMAScript)でフィルタを書ける」だったが、WSHのなかなか目的にたどり着けないオブジェクト階層にイライラするよりも、Node.jsやPhantomJSを使ったほうが精神衛生的にマシだ。
m4
その昔テキスト処理用に触ろうとして、Windows用のどの処理系も日本語の置換に何かしらの問題を抱えていたので泣く泣く諦めた。思うところがあって改めて少し触ってみたが――なるほど、確かに中毒性のある言語*19だ。
Smalltalk (Squeak, Pharo)
Smalltalkは有名な古典的プログラミング言語だというのに、触ったことがない。ということでSqueakとPharoの処理系のみ準備完了。うーん、「環境」付きなのが気になる――言語を弄くる基準が「コンソール上でテキストフィルタ」という変な人種な私だからなあ。
Smalltalk (GNU Smalltalk)
個人の思想信条による理由よりSqueakとPharoにわだかまりを感じてしまう変人なので、邪道だと思いつつもコンソールでテキスト処理もOKなGNU Smalltalkも用意してみた。これで言語としてのSmalltalkの勉強に集中できる……か?
REXX
Open Object REXXの処理系を入手したのに、何故かReginaを入れてClassic REXXっぽい方向に走っていた。何というか、COMコンポーネント.NET Frameworkと無関係でいられるのなら、バッチファイルの代替としてはREXXあたりがほどよい塩梅だと感じる。しかし最近流行の言語とは随分と勝手が違うし、日本語の情報も少ない。メインフレーム以外の世界で流行る可能性は少ないだろう。
T4 Text Template
「へえ、こんなものがVisual Studioに入っていたのか。機能多すぎで色々と便利なツールを見逃しているんだな、やっぱり」と思いつつ触ってみた。テンプレート変換の用途ではピカ一だと思う。ただ処理系を手に入れる方法が「Visual Studioをインストールする」or「MonoDevelopをインストールする」なので、何となく「単体で手軽に使えるツール」ではないというイメージが……。まあC#VBで処理を記述するので、それらの処理系が必要だという面での制約なのだろう。
VBScript on WSH
JScriptほどではないが「Windows上で他人も使えるツールを書くためのLL」扱いしていた言語。Windows Server管理の関係で触っていた。というかWebで入手可能なWSHのサンプルの大半がVBScriptで書かれていたり、ADSI関連のコレクションをJScriptで舐めれなかったりして、結局は必要に駆られて使用することに。明快に記述できる文法は評価に値するが、スクリプト言語としては少々冗長だ。配列は自動拡張しないし、組み込み関数はプリミティブ気味だし、冗長気味な文法との合わせ技でコードがさらに冗長になっていく……。文法や言語仕様の詳細なドキュメントが見つからないのだが、どこにあるのだろうか?*20
Vim script
少し触ってみた分には、exコマンドの拡張(=コマンドの羅列)とは気づかない程度にはプログラミング言語らしいと思う。とはいえ妙なところで嵌ったり微妙に一貫性のない部分があったりするので、その辺りで好き嫌いが別れる気がする。
秀丸マクロ
7年ほど秀丸エディタを使っていたが、マクロを書く機会はなかった。一念発起してFizzBuzzしてみて感じたのは、最近の便利な言語に慣れた身としては色々とモヤモヤ感がある言語仕様だということ(歴史的経緯的に仕方ないのだが)。とはいえちょっとした拡張ツール的なものを手軽に作れそうではあった。

*1:HTML5 + CSS」の組み合わせなら、チューリング完全の疑惑があったり、JavaScript使わずにCSSでWebチャットを作った猛者がいたりと、色々と怪しいのだけど。

*2:「独立性の高い単体のツールを実装する」という視点では、現代ではAWKよりも便利な言語が山ほどある。

*3:しかし標準ライブラリの充実度をJavaC#.NET Framework含む)と比較したり、型推論まわりをKotlinやSwiftと比較してはいけない。以前よりも随分と便利になったのだけど、だけど、隣の芝生を見てしまうと、うーん……。

*4:SwiftではC++の機能を直接呼び出すことができないので、Objective-Cでラッピングして利用することになる(インタフェースはObjective-Cで、内部実装はObjective-C++)。この時、Objective-Cクラスのインスタンス変数として「C++クラスのインスタンスを保持するスマートポインタ」を持つ構成にしておくと、Objective-Cクラスのインスタンスがdeallocされる時に、スマートポインタ経由でC++クラスのインスタンスもdeleteされる。

*5:本格的にC99がサポートされ始めたのはVisual Studio 2013以降だ。それよりも前のバージョンのEOLだが、Visual Studio 2012が2023年1月となっている。

*6:少なくともC++の言語仕様は私の手には余る。自分が把握している範囲の機能でコードを書くのは好きなのだけど。

*7:私の認識では、JavaScriptは、第一級関数やクロージャがお仕事用のメジャーな言語に組み込まれて、少なくない人が使う契機となった言語だ。

*8:Kotlinのvalは「再代入不可の変数」だ(定数はconstで定義する)。Kotlinのプリミティブ型以外のデータ型はclass(つまり参照型)なので、valで定義した変数を破壊的操作する行為は割と普通だと思う。一方でSwiftのletは定数であるし、値型が中心の言語仕様である影響かstructやenum(つまり値型として振る舞うデータ型)が多用されるので、letで定義した変数を破壊的操作できるケースとできないケースが生じる。

*9:でも最近はPythonが多いんだな、これが。

*10:とはいえ、ついついC++を使ってしまうのだよなあ。

*11:これでもsedチューリング完全な言語だと証明されているらしい。

*12:言語仕様的にはC# 5.0の環境だが、ライブラビまわりはC# 4.0相当だったはず。

*13:Windowsのことを考えなければ、自前でライブラリをビルドしてアプリに組み込むのは結構簡単だった。

*14:支障がある部分を触るほど深入りするには、あと20年ぐらい掛かるのではないか?

*15:Schemeの勉強というよりも、再帰の勉強なのか?

*16:現状はirbclispかgoshの3択だ。

*17:――といってもシミュレータだけど。

*18:ClojureScalaに関しては、同様の機能を私が見逃している可能性も高いのだが。

*19:m4はマクロプロセッサなのでプログラミング言語ではないはずだけど……。

*20:MSDNの資料では物足りない。もうちょっと掘り下げた内容のものが欲しい。

Debian stretchからbusterにアップグレードした環境でiptablesからnftablesに乗り換える

Debian 10ではパケットフィルタリングまわりの設定に使用するフレームワークがnftablesに変更されたのだが、この変更が「既に運用中の『iptablesを使用しているDebian 9』」をアップグレードする際にどのような影響を及ぼすのか、メモを残しておく。

Debian LTSでシステムを運用している場合、2022-06-30にDebian 9 LTSのサポートが終了するので、切羽詰まってこれからDebian 10に移行する私みたいな人もいるだろう)

先に要約を書いておく:

  • 前提条件:
    • /etc/iptables直下にrules.v4などの名前でiptablesのルールファイルを置き、netfilter-persistentを使用して起動時にパケットフィルタリングの設定を適用する構成の、Debian 9のシステムを運用している。
    • このシステムにDebian 10をアップグレード・インストールする。
  • 設定ファイルまわりを極力変更したくない人向け:
    • アップグレード時に特に何もする必要はない。
  • アップグレードを機に、パケットフィルタリングの設定管理をiptablesからnftablesに移行したい人向け:
    • おそらくアップグレード完了の時点でnftablesのパッケージはインストール済みのはず。
    • おそらくルールファイルの移行にiptables-restore-translateコマンドは不要。
    • systemdのサービス(netfilter-persistent.servicenftables.service)の切り替えに留意すること。

本題。

Debian 10のリリースノートの記述には、以下のようなことが書かれている。

  • パケットフィルタリングの既定のフレームワークがnftablesに変更された。
  • iptables系のコマンドは、既定では「内部的にnftablesを使用する版」になっている。
    • update-alternativesを使用して「旧来のx_tablesを使用する版」に変更することが可能。

今までiptablesを使用してきた環境をDebian 10にアップグレードした場合、「起動時にnetfilter-persistentiptablesのルールファイルが読み込まれる」という挙動は今まで通りとなる。ただし、使用されるのはiptables系のコマンドだが、内部ではnftablesのフレームワークが用いられている。

つまりパケットフィルタリング設定のフロントエンドはiptablesのまま、内部の処理だけ変更されている訳だ。

だから実用的には、実のところパケットフィルタリングまわりに手を付ける必要はない。今まで通りにiptablesでフィルタリング・ルールを書いておけば、ちゃんと機能する。

一方で、この機会にパケットフィルタリング設定の操作までnftables用のインタフェースに置き換えたいと考える人もいるだろう。

Debian 10にアップグレードして再起動した際、起動時にnetfilter-persistent経由で「内部的にnftablesを使用する版」のiptablesのコマンドにてパケットフィルタリングの設定が適用された状態になっている。

なので、この状態で深く考えずにいきなり以下のような手順で作業することで、nftables用のパケットフィルタリングのルールファイルを生成できる。何しろ元データは起動時にnetfilter-persistent経由で読み込み済みな訳なので……。

# 念のため元のルールファイルを残しておく。
sudo mv /etc/nftables.conf /etc/nftables.conf.old
# nftables用のルールファイルを生成。
printf '%s\n\n%s\n\n' '#!/usr/sbin/nft -f' 'flush ruleset' | sudo tee /etc/nftables.conf >/dev/null
sudo /usr/sbin/nft -s list ruleset | sudo tee -a /etc/nftables.conf >/dev/null
sudo chmod 755 /etc/nftables.conf

Debian管理者ハンドブックnftables wikiのルールファイル移行に関する記述を参考にすると、以下のような手順になりそうな気がするが、これは間違いである。

# iptablesのルールファイルをnft用のコマンドファイルに変換。
sudo /usr/sbin/iptables-restore-translate -f /etc/iptables/rules.v4 >ruleset.nft
# コマンドファイルを適用。
sudo /usr/sbin/nft -f ruleset.nft
# nftables用のルールファイルを保存しておく。
sudo /usr/sbin/nft -s list ruleset | sudo tee /etc/nftables.conf >/dev/null

起動時にnftablesに設定が読み込まれた状態でnft -f ruleset.nftすることになるので、パケットフィルタリングのルールが二重に登録された変な状態のルールファイルになってしまう。

そんなことをしなくても、すでにフィルタリング・ルールは読み込まれているのだから、それをそのままnft -s list rulesetで出力すればよいのだ。

さて、ドキュメント類では微妙に書かれていないが、ルールファイルの移行以外でやるべきことが1つある。それはsystemdのサービスをnetfilter-persistent.serviceからnftables.serviceに切り替えることだ。

切り替えをしないと、せっかくルールファイルを/etc/nftables.confに移行しても、起動時の読み込み元が/etc/iptables/rules.v4のままになってしまう。後日、/etc/nftables.confの内容を変更した際に「再起動するとフィルタリングのルールが元に戻ってしまう」という謎の現象に悩まされることになるかもしれない。

切り替えの手順はこんな感じだろうか?

# netfilter-persistentを停止して無効化。
sudo systemctl stop netfilter-persistent.service
sudo systemctl disable netfilter-persistent.service
# 念のため手動起動も禁止しておく。
sudo systemctl mask netfilter-persistent.service

# nftables.serviceを有効化
sudo systemctl enable nftables.service

これで起動時に/etc/nftables.confのルールが読み込まれるようになる。

余談:iptablesのルールはフラットなので、機械的にnftablesのルールファイルに変換した結果もフラットになる。もしかしたら「よりnftablesらしい」ルールファイルに書き換えたくなるかもしれない。この時、IPアドレスの代わりにFQDNを使ってはならない。手元の環境では、システム起動後にnft -f /etc/nftables.confと手動で読み込ませればFQDNからIPアドレスに変換されるのだが、起動時だとルールの読み込み自体に失敗してしまい、ルール未設定の状態になってしまうようだ。

河豚板のi386版でfiupdateを使いたい

先日Super-microDXにインストールした河豚板をfiupdateでアップデートしようとしたところ、すこしつまづいてしまったので、メモを残しておく。

前提というか背景を書いておくと、Super-microDXにはコンパクトフラッシュのドライブ(リーダ?)が内蔵されている。OSはコンパクトフラッシュにインストールして運用することが多い*1。なお河豚板というかOpenBSDコンパクトフラッシュIDE(パラレルATA)として認識していて、転送モードとしてPIOモード4とUltra DMAモード2が選択されている。

「Super-microDXにインストールした河豚板」と書いているが、これはSuper-microDX上でUSBメモリから河豚板を起動して、usbfadmnewdriveコマンドを使用してコンパクトフラッシュ上に河豚板のLiveUSB版*2を書き込んだ後、コンパクトフラッシュからブートして使用しているのだ。

さて本題。河豚板はOpenBSDのパッチ適用などに合わせてアップデート版がリリースされる。LiveDVD版の場合はメディアを作り直すしかないのだが、LiveUSB版の場合はfiupdateというコマンドを使用して中身を直接更新することができる。

で、このfiupdate、少なくとも河豚板 7.1のi386版では、少しやり方を工夫しないと失敗する。私の環境ではgzip(1)に「No space left on device」と言われてしまった。

河豚板ではRAMディスク上にホームディレクトリ等が展開される。河豚板 7.1ではRAMディスクを実現するためにmfs(Memory File System)を使用しているのだが、i386でのmfsの最大サイズは約1GBである。

つまりホームディレクトリでfiupdateする場合、どう頑張っても1GBのディスクスペース上で作業することになってしまう。で、fiupdateするには1GBでは足りない。

mfsの最大サイズはアーキテクチャごとに異なるようで、amd64では32GB、arm64では16GBと河豚板ガイドに記載されている。だからamd64とarm64では、mfsの割り当てを工夫すればfiupdateがディスクフルで失敗する問題は回避できる。でもi386ではmfsの割り当ての調整ではどうにもならない。

なので、河豚板 i386版でfiupdateする場合は、別途書き込み可能な作業用パーティションを用意して、その中でfiupdateの手順を実行する必要がある。

手元の環境の場合、16GBのコンパクトフラッシュの大半をデータ保存用領域(noasksファイルとかが置いてあるパーティション)に割り当てている。そこで今回はデータ保存用領域を作業用パーティション代わりにしてfiupdateを実行した。

# rootで作業していると仮定:
mount /dev/wd0d /mnt
cd /mnt
ftp https://jp2.dl.fuguita.org/{MD5,FuguIta-7.1-i386-202205061.iso.gz}
fiupdate 202205061

更新はうまくいったようだ。

ただしSuper-microDXの個体の問題なのか、アップデートの最後にデータ保存用領域がうまくアンマウントされなかったようだ。そのため後で「fsck /dev/wd0d」とチェックを走らせることになった。この件はハードウェア側の問題だと思う。

2022-05-31追記:アンマウントの件は、河豚板 7.1 202205161まで存在していた「fiupdateの最後に実行されるrebootが失敗することがある問題」が原因だった模様。この問題は202205161と202205241の2回に分けて修正されたようだ。

*1:一応、USB2.0のポートが2つ付いているので、USBブートも可能である。しかしポート間の物理的な間隔が狭いため、ブート用のUSBデバイスの選択がちょっと面倒である。

*2:コンパクトフラッシュはUSBデバイスではないのだが、河豚板が公式で「LiveDVD版」と「LiveUSB版」という書き方をしているので、それに合わせている。

お願いお助けOpenBSD

10年前に新品で購入したPCにインストールできるメジャーなOSが減ってきた。思案した結果、ひとまずOpenBSDベースの河豚板を使ってみることにした。

件のPCは、ピノーが販売していたSizka Super-microDXという、少し癖のある代物だ。*1

何しろCPUがVortex86DXというx86互換の32bit SoCである。動作周波数は800MHz。メモリはオンボードのDDR2メモリが512MBだ*2

猛者ならFreeDOSを使ったり*3他のPCでクロスコンパイルしたOSを導入したりするかもしれないが、私はめんどくさがりなので、出来合いのビルド済み汎用OSをインストールして使用したい。したいのだが、セキュリティを気にして現行のOSから選択しようとすると、候補が少ないのだ。

インストール可能なOSが減ってきた理由は、Vortex86DXがサポートする命令セットの古さにある。Vortex86DXはi586*4の命令セットに概ね対応している*5。このSoCでOSを動かすためには、OSのバイナリはi586以前の命令で構成されている必要がある。i686*6以降の命令が含まれていると、実行時に該当する命令にたどり着いた時点でCPU例外が発生してストールする。

最近のLinuxx86-64のみのディストリビューションも多いのだが、x86(32bit)版をリリースしているものであっても、その中身は「i686以降をサポートする」だったりする。多種多様なアーキテクチャをサポートしているDebianでも、Debian 9 (stretch) にてi586はサポート対象外となっている*7

FreeBSD i386も、13.0-RELEASE以降ではi686が最低ラインだ。一方で12.x系はi586でも動作する……はずなのだが、なぜか12.2-RELEASE以降のGENERIC kernelのバイナリにはi686命令であるCMOVが含まれているため、i586では動作しない。この問題はFreeBSDのBugzillaに挙がっているようだが、先行きは不透明だ。

どうしたものかと思案していたところで、Vortex86DXを搭載したPCにてNetBSD 9.2とOpenBSD 7.0が動作したとの情報を発見した。

NetBSDOpenBSDなら、最近のビルドでも問題なく使えるようだ。

ということで勉強もかねて触ってみようと思ったのだが、誤解かもしれないが素のNetBSDOpenBSDはどちらもインストールが(他のOSと比較して)面倒くさい印象があるのでパス。NetBSDておくれLive Imageは良さそうだが、Vortex86DXで動かすには重そうなので*8、今回はパス。河豚板は起動時にコンソールを指定できるだけでなく、設定で永続化できるので、丁度よさそうだった。

今回は、あまり手間をかけずに環境を構築できる河豚板を使用して、OpenBSDCLI環境を構築することにした。まだ河豚板 7.1 202205011を入れただけの状態だが、今のところ問題なく動作している。

後で気づいたのだが、個体の問題なのか私のSuper-microDXは熱が原因らしきバスエラーが発生することがあるので、システムの利用と設定等の保存(ファイルシステムへの書き込み)を分離できる河豚板は割とマッチしているかもしれない……設定保存中にバスエラーが起きませんように。

*1:メディアなどで「煙草箱サイズ」と書かれていたが、個人的には、サイズ感はAppleの60W MagSafe電源アダプタに近いと思う。アレを1.3倍ぐらい厚くした感じだ。

*2:メモリは333MHz駆動らしいのだが、DDR2のメモリチップ規格に333MHzなんて存在しないので、意味を図りかねている。Vortex86DXのDDR2コントローラはDDR2-667までサポートしているようだが、DDR2-667(つまりメモリチップが667MHz駆動)の時のバスクロックは333MHzなので、このことを指し示しているのだろうか? それともクロックを下げてDDR2-400(400MHz駆動)よりも低い333MHzでメモリチップを動作させているのだろうか?

*3:CPU内蔵のフラッシュメモリがフロッピディスクドライブとして認識できるようになっていて、しかも出荷時にFreeDOSがインストールされている。

*4:初代PentiumMMX Pentiumの世代。

*5:噂によれば、実は一部のあまり使われない命令には対応していないらしいが……。

*6:Pentium ProPentium II~4の世代。

*7:正確に言えば「NOPL以外の全てのi686命令に対応しているi586 CPU」はサポートしている。でもそれって「ほぼi686」だよね。

*8:GUI前提っぽいので。

プログラマはコミュ障じゃないよ

時にコミュ障とプログラマがセットで語られることがあるのだが、そもそもプログラマに占めるコミュ障の割合は他の職業と比較して有意に大きいのだろうか、という疑問がある。

世間一般でいう「コミュニケーション能力」が具体的にどのような内容であるのか、寡聞にして知らないのだが、職業プログラマにとって効率的なコミュニケーションとは次のようなものだ。

  1. 正確な情報を、
  2. 包み隠さず全て、
  3. できるだけ正確な用語を使用して、
  4. 簡潔かつ網羅的に、
  5. キャッチアップしやすい媒体にて、
  6. なるべく容易に再利用できる形式で、
  7. 概ね全ての関係者がアクセスできる方式にて発信する。

コンピュータ・システムの開発ってのは、賽の河原で石を積み上げてサグラダ・ファミリアを建造するようなものである。

プログラマは、その難しさを誰よりも知っている訳で、だからこそ情報の正確さと、チーム内の情報格差の是正を重視する。情報が不正確だと迷走するし、情報格差は意図せぬ同床異夢を誘発する。どちらも、バベルの塔の逸話のごとくプロジェクト関係者の言葉を乱して混乱と疲労を生むだろう。いずれもシステム完成の障害でしかない。

現実には、政治ムーブの一環として、言葉を濁したり、相手によって情報の公開/非公開を取捨選択したり、些細な言葉尻を捕らえて責任をなすりつけあったりする、なんてことが多いのだが、実のところそのような行為は「システムを完成させる」という技術的視点では第一級の障害である。火薬庫の屋根裏で火遊びするに等しい。

というかコンピュータ・システムという複雑な複雑怪奇な代物を作ろうというのに、何でこの人たちは悠長に政治ムーブしているのだろう、ちょっとは空気読めよ――というのがプログラマの本音である。

つまり「コンピュータ・システム開発」という限定された文化圏においては、「プログラマはコミュ障ではなく、プログラマ以外がコミュ障である」というケースも多いのだ。

まあ、そうは言っても予算を引っ張ってくるのは政治ムーブに長けた人たちである。例えコミュ障であっても、金を握っている奴は強いのだ。そして時折彼らの目的は「システムの完成」ではない。「システムの完成」ではなく、それに付随すること――例えば「賞賛を得たい」とか、逆に「失点しない」とかが目的だったりする。だから、たとえ「システムの完成」に有効な施策であっても、彼らの真の目的に合致しないなら、すげなく拒否されるのである。かなわんなあ、ほんと。

余談:ところでプログラマプログラマ以外の人たち(主に顧客や多重下請け構造の元請社員など)の間の温度差は、プログラマ以外の人たちはプロジェクト内において「コンピュータ・システム開発技術」の技術面の能力が相対的に低いことがある、という所からも生じているように思う。ダニング=クルーガー効果より、彼らは「コンピュータ・システム開発の技術的側面」について自分たちを過信してしまうことがあるのだ。だから余裕がないのに余裕があると思い込んで悠長に政治ムーブをかましてくる上に、事の重大さを「大したことはない」と誤認して「何でできないの? 簡単でしょ?」と上から目線で言い放つ人が稀に出現するのである。かなわんなあ、ほんと。

余談その2:なおダニング=クルーガー効果はプログラマにも当てはまる。だからこそ、彼らコミュ障を侮ってはならない ;-)

さよならSDelete

新しいPCに環境を移行してから、SDeleteを使う機会が激減した。ここ1~2ヶ月は1度も使用していない。

何のことはない、ようやくSSDモデルのPCに買い替えたのだ。

以前のPCは2013年に購入したノートPCだ。当時は「クラウド・サービスのストレージにデータを置く」という行為が今ほど一般的ではなく、PC本体にデータを丸抱えしがちだった。実用レベルのストレージ・サイズをSSDで賄うには少々高額で、まだまだHDDモデルが圧倒的に多かった時期だったと記憶している*1

SDeleteを多用していた理由は、ストレージがHDDだったからだ。センシティブな情報を含むファイルはSDeleteで削除していたし、ディスク交換などのタイミングではストレージ全体にSDeleteをかけることもあった。

便利に使っていたSDeleteは、しかし、HDDに特化したツールだった。

SDeleteや、OS X Yosemite 10.10まで存在したsrm(3)、そしてGNU Coreutils付属のshred(1)などによる「指定したファイルの削除」では、ストレージ上の「指定したファイルのデータが存在するディスクセクタ」に、特殊なデータ・パターンを複数回上書きする。これにより、後でディスクセクタから「元のデータとは全くかけ離れたデータ」しか読み取れなくする――という仕組みで、データの復元が困難になるようにしている。

残念ながら今日のSSDはウェアレベリングが行われているため、SDeleteによる「特殊なデータ・パターンによる上書き」のデータは、おそらく「指定したファイルのデータが存在するブロック」ではなく「別のブロック(消去済みブロック)」に書き込まれてしまうだろう。SDeleteはユーザランドのツールであり、OS・デバイスドライバSSDファームウェアからは「通常のデータ書き込み」と「SDeleteによる『特殊なデータ・パターン』の書き込み」を区別できないのだ。

SSDではSDeleteの効果が期待できないのならば、どうすればよいのだろうか?

実のところ、SSDの場合は何もしなくても「HDDより安全」ではないかと思う。なぜなら今ではSSDもOSもTrim命令に対応しているからだ。Trim命令による「空いたブロックのデータ消去」は、データ復旧業者が「SSDのデータ復旧が難しい理由」の1つに挙げている項目だ。普段から長時間PCを使用しているならば、バックグラウンドでそこそこの頻度でTrim命令に基づくデータ削除が行われているだろう。

普段は「Trim命令によるデータ削除」に期待しつつ、ディスク交換の時にSSDメーカのメンテナンス・ツールでSSD全体を完全削除する――という運用でも、HDDよりも遥かに「削除したファイルの復元」が難しくなるように思う。

……とはいえ、Trim命令に応じて実際にブロックのデータを削除するのはSSDファームウェアな訳で、実際にどの程度の頻度でブロックが消去されるのか不透明だ。

それに、SSDについて素人な私は「メンテナンス・ツールで完全削除したはずのブロックからデータを復元できてしまう可能性は?」とか「そもそも完全削除する前にディスクがお亡くなりになった場合に、NANDチップからデータを吸い出して復元することは可能なのだろうか?」とか、色々と余計なことを考えてしまうのである。

そこでディスク全体を暗号化した。WindowsならBitLocker、macOSならFileVaultだ*2。暗号化しておけば、仮に「完全削除したブロックからデータを復元できる」としても、復元したデータは暗号化されている。また完全削除する前にディスクが死亡した場合も、仮にNANDチップからデータを吸い出して復元することが可能だとしても、そのデータは暗号化されている。どちらも、「実は簡単に復号できちゃう」みたいな欠点がない限り、データの復元は困難なはずだ。

ディスク暗号化は、PC故障時に生き残ったディスクからデータを吸い出すのが困難になるという欠点を伴うのだが、そもそもディスク故障まで考慮してデータ・バックアップ体制を構築しておく方が健全だろう。

PCのライフサイクルとしては、最初に環境構築を行う前にディスク全体を暗号化しておき、普段はOS標準の機能でファイルを削除しつつTrim命令に期待して、ディスク交換の際には「暗号化したままの状態で、メンテナンス・ツールでSSD全体を完全削除する」、という流れとなる。

そんな訳で、今では暗号化されたSSDの上で、センシティブな情報を含むファイルもOS標準の機能で削除している。

SDeleteそのものは、USB-HDDとUSBメモリ*3を物理破壊する前に念のため完全削除する作業や、仮想ディスクのダイエットのために空き領域をゼロクリアするために、ディスクの片隅に眠らせている。でも普段使いしなくなったので、もうヘルプメッセージを見ないと使い方が分からなくなってしまった。

*1:この後、2014~2015年ごろからミドルレンジのノートPCでもSSDモデルが見られるようになったと思う。

*2:Linuxは、ディストリビューションによってはインストール時に暗号化LVMを選択できるので、それを選択するのが手っ取り早い気がする。

*3:SSDUSBメモリも記憶素子としてNANDフラッシュを用いているが、大半のUSBメモリにはウェアレベリング機能がない。

Better C的なC++の使い方

C++をあまり深入りせずにBetter Cとして使うとこうなる。

事前準備:文法面の細かな差異を押さえる

細かいところにC言語C++の非互換な部分があるので、それを押さえておく。例えば:

C95以前のCプログラマ向け:ブロック先頭以外で内部変数を宣言する

C99以降ではブロック先頭以外でも内部変数を宣言できるようになったが、お仕事でC言語を使っているとC95以前が絡むことも多いので、ブロック先頭でまとめて宣言する癖がついているベテランも多いと思う。

/* C95以前のC言語の場合 */

#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が絡んでくるので、内部変数は使う直前に宣言・初期化するようにする。

// C++の場合
// あえてC言語由来の機能を使用している

#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

/* C95以前のC言語の場合 */

/* ブール型がなく、独自定義もされてないケース */
static int valid_number(const char *s);

/* 独自定義のブール型 BOOLEAN があるケース */
static BOOLEAN valid_token(const char *s);
/* C99以降のC言語の場合 */

#include <stdbool.h>

static bool valid_number(const char *s);
static bool valid_token(const char *s);
// C++の場合
// あえてC言語由来の文字列を使用している

static bool valid_number(const char *s);
static bool valid_token(const char *s);

ファイルスコープの代わりに無名名前空間を使う

ファイルスコープと書いたが、要はstatic関数やstaticなグローバル変数のことだ。

/* C言語の場合 */

static int foo;

static int triple(const int n)
{
    return n * 3;
}

C++的には、staticではなく無名の名前空間を使うことが推奨されている。

// C++の場合

namespace {

int foo;

int triple(const int n)
{
    return n * 3;
}

} // namespace

Better Cの観点では、この項目の優先度は低い気がしないでもない。

とはいえ、よく考えれば毎度毎度staticを書くのも面倒な訳で、コードを書く時の流儀として「ファイル先頭側に『モジュール内部で使用する非公開関数群』をまとめて記述して、ファイル末尾側に『外部に公開する関数群』を記述する」というスタイルを採用している人ならば、staticから無名の名前空間に切り替えるのは悪くない判断だと思う。

……公開関数と非公開関数を混在させるスタイルの人にとっては扱いにくいだろうけど。

C++11以降:cstdint(stdint.h)の型を使う

やや組み込み系っぽいネタ。

これ、C99以降のC言語を使う場合もそうなのだが、厳密な大きさ(ビット幅)を指定したデータ型を用いるのに、そろそろ独自のtypedefではなくint8_tやuint16_tを使っても許されるのではないかと思うのだ……。

/* C95以前のC言語の場合 */

/* 独自の型定義が書かれている共通ヘッダファイル */
#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];
/* C99以降のC言語の場合 */

#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];
// C++11の場合

#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];

} // namespace

ちなみに、C++11的にはcstdintをインクルードするべきなのだが、現時点では移植性の問題でstdint.hをインクルードした方が安全なこともあるようだ。

C++03以前:constによる定数を積極的に使う

C言語では、定数を定義する方法は2通りある。マクロ置換を使う方法と、enumの列挙子を使う方法だ。

/* マクロ置換を使う場合 */
#define FOO_LIMIT 128
/* enumを使う場合 */
enum {
    FOO_LIMIT = 128
};

C++では、const修飾子を使用して整数や浮動小数点数を定数化することも可能だ。定数を定義する方法が増えている。

// C++03の場合
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化できる)はず」と解釈してコンパイルを試みるはずだ。

// C++03の場合
static const std::size_t FOO_LIMIT = 128;
// C++11の場合
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キーワードを用いることで、初期化子から推論した型を適用させることが可能になった。

// autoを使わない場合

int n = 1;
const unsigned long maxval = 255;
const char *s = "";
const char *p = s;
// autoを使う場合

auto n = 1;
const auto maxval = 255UL;
auto s = "";
const auto *p = s;

例えばイテレータのように、標準ライブラリの機能に付随する型については、毎度毎度自分で長ったらしい型名を書いたり、typedefなどで省略形を定義して使うよりも、autoを使ったほうが楽だろう。

// 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()) {
    // 見つからなかった
}
// autoを使う場合

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だと誤解されてしまう君には、少しうんざりしていたんだ。

/* C言語の場合 */

const char *p = NULL;
// C++03の場合

const char *p = NULL;
// C++11の場合

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)
{
    /* 明示的なNULLチェックが必要な場合 */
    if (foo == NULL) {
        /* エラー処理 */
    }
}

static void func_bar(struct STRUCT_FOO *foo)
{
    /* 明示的なNULLチェックをしない場合 */
    assert(foo != NULL);

    /*
     * NULLをつっこむ上位ルーチンが悪い!
     * (NULLチェックは上位側でやる約束だったよな?)
     * テストで洗い出せ!
     */
}

NULLチェックのような細々した処理が不要となる参照は、便利といえば便利だ。

// 参照の場合

static void func_foo(const STRUCT_FOO& foo)
{
    // NULLチェックは不要。言語仕様的にありえない……はず!
}

もっとも、const参照の引数ならともかく、非const参照の引数の場合、その関数を呼び出す側にて例えば「foo(bar);」というコード片からbarが変更される可能性を予期しにくいという問題があって、参照を使うかポインタを使うか意見が割れるところだが……。

// 参照の場合

func_foo(baz);  // 関数内でbazを変更していない
func_bar(baz);  // 関数内でbazを変更している

// bazを変更しているか否か、上記コード片からは不明

しかし冷静に考えると、それは構造体のポインタでも一緒だ。

// ポインタの場合

func_foo(&baz);  // 関数内でbazを変更していない
func_bar(&baz);  // 関数内でbazを変更している

// bazを変更しているか否か、上記コード片からは不明

要するに、通常のポインタを使っていて構造体のメンバを変更しているのか、constポインタでメンバを変更できないようにしているのか、呼び出す側のコード片からは判断できない。

というか、似たような(しかし全く異なる)問題は他のメジャーな言語でも見られる。例えばRubyの変数はポインタのようなもので、メソッドの引数は値渡しだ。なので、メソッド内で引数が参照しているオブジェクトを破壊的に操作すると、変化が波及する。メソッド内でオブジェクトを破壊的に操作しているか否かは、そのメソッドを呼び出す側のコード片からは判断できない。

# Rubyの場合

foo(baz)    # メソッド内でbazを破壊的に操作していない
bar(baz)    # メソッド内でbazを破壊的に操作している

# bazを破壊的に操作しているか否か、上記コード片からは不明

まあ、C++の参照はポインタとは異なる代物なので、Rubyの例と同一視はできないのだが、しかし「メソッドを呼び出しているコード片からは、引数に設定した値が変化してしまうか否かが分からない」という点は同じだ。

そう考えると、深く考えずに非const参照の引数を使っても構わない気がしないでもない。

あとポインタを返す関数を参照を返すように単純に置換できるかというと、意外と「通常はオブジェクトを指し示すポインタだが、異常発生時にはNULLポインタを返す」という仕様の関数が多くて、一筋縄ではいかない。まあ、ポインタと参照は別物だからしかたない。この場合は「異常発生時にはNULLポインタを返す」という部分を「エラーコードを返す」や「例外をthrowする*6」などに仕様変更することになる。

キャスト演算にはC++のキャスト演算子を使用する

キャスト演算したい時、従来のC言語流のキャストではなくC++のキャスト演算子を使用した方がよい。コードの記述はやや冗長になるが、結果としてC言語流のキャストを使用した場合よりも安全なコードになりやすい。

/* C言語の場合 */

static void func(uint8_t *data)
{
    /* 中略 */

    uint16_t n = (uint16_t) strlen((const char *) data);

    /* 後略 */
}
// C++の場合

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つのキャスト演算子に整理している。

  1. const_cast
  2. dynamic_cast
  3. reinterpret_cast
  4. static_cast

ということで、C++のキャスト演算子を使用するという事は、強制的にプログラマに「このキャストは、どのような役割のキャストか?」と自問させて明記させるに等しい。考えさせる分だけ、結果としてコードが安全になる可能性が少しばかり上がるはずだ。

蛇足:なおObjective-C++においては、NSIntegerのようなプリミティブ型はC++のキャスト演算子でキャストできる一方で、Objective-Cのクラスのオブジェクトのアップキャスト/ダウンキャストにはC言語流のキャストを使用しなくてはならない。

名前の衝突回避に名前空間を使う(foo_func()ではなくfoo::func())

C言語でライブラリやモジュールを実装する際には、他のライブラリ等との名前の衝突を回避するために、名前のプレフィックスにライブラリ名を付けることが多い。

/*
 * C言語の場合
 * 例えばモジュール foo を実装するなら:
 */
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++では名前空間を使う。

// C++03の場合
// 例えばモジュール foo を実装するなら:
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();

} // namespace foo

念のため書いておくと、複数のコンテクストを扱いたいモジュール*7は、大抵は名前空間ではなくクラスで実装して、各コンテクストごとにオブジェクト化してしまうほうがよい。一方で、複数のコンテクストを扱うことなど到底ありえない場合*8は、他のオブジェクト指向プログラミング言語ならシングルトンないし「クラスメソッドonlyのクラス」を使うところだが、C++ではベタに名前空間を使っても許されると思う。だってC++なんだよ?

構造体やenumをtypedefしない

C言語では、例えばstruct FooをFooと書くためにtypedefしておく必要があった。

/* C言語の場合 */

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と書くことができる。

// C++の場合

struct Foo {
    // メンバは省略
};

struct Bar {
    // メンバは省略
};

static void func()
{
    struct Foo foo1;
    Foo foo2;
    Bar bar;

    // 以下略
}

全くの別名を付けたいのでもなければ、構造体やenumをtypedefする必要はない。

C++11以降:typedefよりもusingで別名を付ける

ところでC++11以降ではusingキーワードを使って型に別名を付けることができる。

// C++03の場合

typedef std::int32_t error_type;
// C++11の場合

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に演算子オーバーロード以外のメンバ関数とか要らない気もするのだが……。

ただし、コンストラクタは構造体でも有用だ。メンバを初期化するコードを書いておけば、その構造体のオブジェクトをどこで定義しても、必ず同じ内容に初期化される。

/* C言語の場合 */

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、foo2を明示的に初期化する必要がある。
     * 定数もどきか初期化関数を使えば、常に同じ初期値に初期化される。
     */
    foo1 = Foo_INIT_VALUE;
    foo_initialize(&foo2);

    /* 以下略 */
}
// C++03の場合

struct Foo {
    int a;
    int b;

    Foo : a(0), b(~0) {}
};

static void func()
{
    Foo foo1, foo2;

    // foo1、foo2の明示的な初期化は不要。
    // 常に同じ初期値になっている。

    // 以下略
}

C++11以降:構造体のメンバの初期化にクラス内初期化子を使う

ところでC++11以降では、非静的メンバ変数ならコンストラクタを使わずに初期化式にて初期化パラメータを記述することもできる。

// C++03の場合

struct Foo {
    int a;
    int b;

    Foo : a(0), b(~0) {}
};
// C++11の場合

struct Foo {
    int a { 0 };
    int b { ~0 };
};

あるメンバ変数において、クラス内初期化子と「コンストラクタのメンバ初期化子」の両方が存在する場合は、メンバ初期化子の方が優先される。

個人的には、C++11以降では「既定値による初期化」にはクラス内初期化子を使用しておき、必要に応じてコンストラクタのメンバ初期化子を用いて「インスタンス生成時にユーザが既定値以外の値で初期化する」ことを可能にしておく、みたいな感じの使い分けがベターかなと思っている。

構造体の初期化にmemset(3)を使わない

そもそもC言語でも構造体をmemset(3)でゼロクリアする行為は割とダーティハックなのだけど*11C++の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)を定義する際に型名の後ろに付与することで、その構造体から派生(継承)することができなくなる。

// C++03の場合

struct Foo {
    int a;
    int b;

    Foo : a(0), b(~0) {}
};
// C++11の場合

struct Foo final {
    int a { 0 };
    int b { ~0 };
};

※finalを書く場所に注意すること。C++のfinalは一般的なキーワード(予約語)ではなくコンテキスト依存キーワードである。所定の場所に記述した場合のみキーワードとしての効力を発揮するが、それ以外の場所では識別子として扱われる。

基本的に、Better Cで構造体を定義する際に「派生(継承)によるクラス階層の構築」を意識して設計・実装することは極めて稀である。つまり定義された構造体は、基底クラス(スーパークラス)として用いるには不適切な代物である可能性が高い。なので積極的にfinalを付与して派生(継承)を禁止しておく方がよいだろう。

同様に、Better Cからのステップアップでクラスを定義するようになった場合には、特定の課題の解決に特化したクラスを直接記述することが多く、派生(継承)を考慮した設計・実装になっていないことが多い。このような場合にも積極的にfinalを付与しておく方がよいだろう。

一方で「派生(継承)によるクラス階層の構築」を意識してクラス群を設計・実装する場合には、finalを控えめに使用するべきなのは言うまでもない。

構造体のオブジェクト同士で演算したいなら演算子オーバーロードを使う

頻度は低いが、時々構造体のオブジェクト同士で演算を行いたいことがある。可能性としては、比較演算(特に一致・不一致)が多いだろう*12

この時、専用の関数やメソッドとして演算処理を実装するのではなく、演算子オーバーロードを使用しておくと、標準ライブラリのalgorithmや関数オブジェクトと組み合わせて処理を記述できる可能性が高くなり、後々の実装が楽になる……かもしれない。

/* C言語の場合 */

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;
}
// C++03の場合

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なら、デフォルトで比較演算子が定義されている。前提として、メンバ変数の型にて比較演算子が定義されている必要がある。

// C++03の場合

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;
    }
};
// C++11のstd::tupleを使用する場合

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を基底とする派生クラスを定義してアクセサを用意するべきだろう。

// C++11の場合

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++を使うシチュエーションで、そこまでシビアな条件である機会は少ないはずだ。

/*
 * C言語の場合
 *
 * よくありがちなインタフェース。
 * 実装する際には、色々と考えなくてはならない。
 * - 引数にNULLを突っ込まれた場合にどうするか?
 * - dst_sizeにはヌル文字分を含める? 含めない?
 * - 一々書き込み先の大きさを気にしなくてはならないのが面倒。
 * - 書き込み時にヌル終端を忘れずに……。
 */
extern void foo(const char *src, char *dst, const size_t dst_size);

C言語ならともかくC++なのだから、std::stringやstd::stringstreamで幸せになりましょうよ……なれるかなあ。

// C++の場合
//
// - 参照を使っていることもあり、NULLチェック不要。
// - 書き込み先の大きさは、とりあえず気にしなくてOK。
//   (メモリ不足の可能性との戦いはあるが……)
// - 普通に使う分には、ヌル終端を忘れることはないはず。
extern void foo(const std::string& src, std::string& dst);

状況次第では、標準ライブラリにこだわる必要はなくて、CStringやQStringでも構わないのだが、少なくとも文字列専用の型を用いるべきだろう……特段の事情があるのなら話は別だが。

ホスト環境:配列よりもstd::vectorやstd::arrayを使う

用途次第だが、配列よりもstd::vectorC++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)]) {
    // 見つからなかった
}
// std::vectorの場合
// (このケースではstd::arrayの方が妥当かも)

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に設定する要素をまとめた配列を別途用意しておき、コンストラクタの引数に指定する必要があったが、その手間がなくなった。

// C++03の場合

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()) {
    // 見つからなかった
}
// C++11の場合
// (このケースではstd::arrayの方が妥当かも)

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文を使う実装がよく見られる。

/* 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:   /*FALLTHROUGH*/
	default:            return BAR_UNKNOWN;
	}
	/*NOTREACHED*/
}

少し工夫してデータ構造を用いる場合は、配列を使ったり(インデックス番号から、当該インデックスの要素にマッピング)、構造体の配列を使ったり(メンバAの値から、メンバBの値にマッピング)することになる。

/* C言語の場合:配列版 */

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];
}
/* C言語の場合:構造体配列版 */

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を使うことができる。

// C++の場合

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> のようなもので実現できる。

// C++の場合

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文で舐めているのは……色々な事情があってそうしているのならともかく、何も考えずに新規コードで常にそう書いているのを見ると「あまり近代的ではないな」と感じる。

// for文の場合(その1)
// (このケースではstd::arrayの方が妥当かも)

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()) {
    // 見つからなかった
}
// for文の場合(その2)
// (このケースではstd::arrayの方が妥当かも)

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も悪くはない。悪くはないが、プリミティブなループ処理だ。

// Range-based for loopの場合
// (このケースではstd::arrayの方が妥当かも)

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の関数テンプレートでは、もう少し抽象的な機能を提供しているので、漁ってみる価値があるだろう。

// std::findの場合
// (このケースではstd::arrayの方が妥当かも)

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++のクラスが入り混じってカオスなところに、さらにブロック構文とラムダ式が追加されるだなんて、憂鬱だ。

// Objective-C++(C++11以降)でブロック構文を使用した場合の例

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の競演で目が回るのである。悪酔いしそう。

関数テンプレートを使う(クラステンプレートは無理でも……)

型は違えどコードの見た目は瓜二つ、というコードを見かけた場合、関数テンプレートを使うことで、型を超えてコードを抽象化(一般化)できる可能性がある。

例えば値の範囲をチェックして、最小値や最大値から外れた値を範囲内にまるめたい場合、次のような関数テンプレートを用意しておけば、型に関係なく使いまわすことができる。

// C++03の場合
// (※C++17では同様の機能を提供するstd::clamp()が追加された)

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を付与することで、パフォーマンスの向上を期待できる。

// C++11の場合
// noexcept の代わりに noexcept(true) と書くことも可能

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型の変数に容易に変換できてしまった。

/* C言語の場合 */

enum KeyCode {
    KeyCode_A,
    KeyCode_B,
    KeyCode_C
};

// 中略

enum KeyCode key_code_1 = KeyCode_A;
int          key_code_2 = KeyCode_B;

C++03では、名前の衝突については、名前空間や構造体を用いることでも回避できた。しかし型チェックの問題は依然として残ったままだった。

// 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)を用いることで、名前の衝突だけでなく型チェックの問題も回避できるようになった。型付けの制約から逃れたいならば、明示的にキャストを用いる必要がある。

// C++11の場合

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

/* C99以降のC言語の場合 */

#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)し忘れていて、メモリリークが発生する」というものだ。

/* C99以降のC言語の場合 */

#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) {
        /*
         * エラーコードが返ってきたので中断
         * →あれ? free(3)し忘れている……?
         */
        return false;
    }

    /* 中略:バッファを使って色々と作業 */

    /* 使い終わった作業用バッファを解放 */
    free(buf);

    return true;
}

C++ではどうか? malloc(3)/free(3)のペアをnew/deleteに置き換えただけでは、同様の問題が発生する可能性がある。そこで標準ライブラリのstd::vectorを内部変数として利用する。std::vectorはRAIIに則しているので、関数を終了して内部変数が破棄されるタイミングでデストラクタが実行されて、内部で確保しているだろうヒープメモリが解放される。なのでメモリリークは発生しない。

// C++の場合

#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) {
        // エラーコードが返ってきたので中断
        // →bufは内部変数
        // →関数終了時にデストラクタが呼ばれてリソース解放
        return false;
    }

    // 中略:バッファを使って色々と作業

    // 使い終わった作業用バッファを解放しなくとも、
    // 関数終了時にデストラクタが呼ばれてリソース解放

    return true;
}

} // namespace

(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::lock_guard<std::mutex> lck(sync_mutex);

    // 中略:排他が必要な処理を行う

    // スコープを抜ける時、デストラクタでアンロックされる
}

つまり途中returnだろうと何だろうと、スコープを抜けた時にアンロックされる。

特に排他区間のコード行数が長くなるケースや、排他区間内で「例外をthrowする可能性がある関数」を呼び出す可能性があるケースでは、デストラクタでミューテックスをアンロックするstd::lock_guardは非常に重宝する。

もしも組み込みでC++が利用可能ならば、リアルタイムOSAPIのラッパーとして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を用いる旨を書いたが、同様のことはスマートポインタでも実現できる。

// C++11のstd::unique_ptrを使用する場合

#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) {
        // エラーコードが返ってきたので中断
        // →bufは内部変数
        // →関数終了時にデストラクタが呼ばれて、deleteでメモリ解放
        return false;
    }

    // 中略:バッファを使って色々と作業

    // 使い終わった作業用バッファを解放しなくとも、
    // 関数終了時にデストラクタが呼ばれて、deleteでメモリ解放

    return true;
}

} // namespace

ただし、スマートポインタをバリバリ使用するためには、どこかの時点で右辺値参照やムーブセマンティクスについてある程度理解する必要がある。これって、CプログラマC++03時代のプログラマからすると、微妙にハードルが高いかもしれない。

でも「ムーブによる所有権の移動」という考え方は学ぶ価値がある。先に書いたように「自前で動的メモリ確保するコードを書くことが極端に少ない」私であっても、リソース管理の観点で色々と考えた結果、std::unique_ptrを選択したことが1回、std::shared_ptrを選択したことが2回ある――というぐらいには、ホスト環境向けのコードを書く時に「タンスのいちばん下の引き出しにしまわれた道具」として出番がある。他の人ならば、もっと出番があるだろう。

まとめ

C++は難しくてよく分からない……まあ、私は所詮自称Cプログラマで、『Effective C++』すら読んでいないので*23C++ガチ勢からすると妙なコードを書いていると思う。

*1:『Code Complete 第2版 上』の10.3~10.4を参照。

*2:正確に言えば、整数や浮動小数点数リテラルは型を持っているし、接尾辞によって型を明示することも可能である。しかし私を含めて、リテラルの型を明示せずに使用する怠惰なプログラマが多いのも事実である。これに「マクロ置換による定数定義」が加わることで、変数と定数の型が食い違う(なので暗黙のうちに型変換が発生している)コードが出来上がることになる。

*3:C++14でconstexpr関数における制限が緩和されて扱いやすくなったため。constexpr関数は呼び出し時に「引数が定数であり、かつ呼び出し式の左辺がconstexpr変数である」ならばコンパイル時に計算が行われる――という特徴はプログラミング時の札の切り方に変化をもたらすと思うが、少しばかりBetter Cとは乖離があるからなあ。

*4:例えばVisual Studio Codeとか。

*5:ジェネリックラムダのこと。

*6:ホスト環境向けのコードを書いているならば、stdexceptに定義されている標準的な例外クラスを使用してもよいだろう。

*7:Windows APIに例えるなら、ハンドルを取得・参照するような設計のモジュールのこと。

*8:例えばMathクラスのような、クラスメソッド/静的メンバ関数しかもたないもの。

*9:正確には型ではなくテンプレートなのだが、Cプログラマを想定して、あえて分かりやすい(そして不正確な)表現を用いている。

*10:デフォルトアクセスが違うだけ。classではprivate、structではpublic。

*11:memset(3)による構造体のゼロクリアでは、構造体の実体が存在するメモリ領域をint型の0のビットの並びで埋め尽くす、という処理を行っている。ところで大抵の環境ではfloatやdoubleなどの「浮動小数点数における0でのビットの並び」と「int型の0のビットの並び」は異なる。また、CやC++の標準規格においては、コンピュータのハードウェア上における「ヌルポインタを意味する値のビットの並び」について定義されていないので、「int型の0のビットの並び」と同じか否かは環境依存の話となる。よって、少なくともfloat/double/ポインタ型のメンバ変数を持つ構造体をmemset(3)でゼロクリアする行為は不適切である、と考えた方がよい。

*12:構造体のオブジェクト同士で算術演算や論理演算を行いたいケースは稀では? まあ、加算ぐらいならありそうだが。

*13:コンパイラにもよるが、「関数ポインタでは関数呼び出しがインライン化されず、関数オブジェクトではインライン化される」という可能性がある。

*14:2022年2月時点の話。正直なところObjective-Cのコードの中で「バリバリのC言語の流儀」でCore MIDIAPIを叩く方が扱いやすい気がする。

*15:Objective-Cのクラスのオブジェクトのキャストは、C言語流のキャストの構文で書かなくてはならない。でも時々手が滑ってC++のキャストの構文で書いてしまうのである……。

*16:誤って異なるenumの列挙子やあさっての整数定数を代入してしまったり、算術演算をしてしまうことで、enum変数の中身が自身の型の列挙子と一致しなくなる危険のこと。

*17:言語規格としては、フリースタンディング環境でもstd::atomicは用意されているはずである。

*18:偏見である。

*19:そしてJava普及期に初めてGCに触れたプログラマjava.lang.Object.finalize()に振り回された遠因でもあるような気がする。Javaに限らず、この手の機能はGCのタイミングに依存するものであるし、言語仕様次第だがJavaのように「実行順序は不定」とか「実行されずにプロセスが終了することもある」のような制約があるものだ。

*20:C99で追加された可変長配列は、以前よりVisual Studioでは未サポートだった上に、C11でオプション扱いになってしまった。特定の環境向けでも良いのならば、LinuxBSDのalloca(3)や、Windows APIの_alloca()/_malloca()もあるが、_malloca()を除けば、C99の可変長配列を含めて「確保可能な最大サイズは、スタックサイズの影響下にある」といえるだろう。あと_malloca()は_freea()とセットで使うこと。

*21:タスク間で排他が必要な部分をバイナリセマフォミューテックスでガードするこ とがあるので。

*22:まず「特定の条件で」「異常処理が走るケース」が不明なところから調査開始するのが大半なので……。

*23:一応『C++のエッセンス』『Effective Modern C++』『プログラミング言語C++ 第4版』は本棚にあり、時々参照しているけど。

汝テストアプリと侮るなかれ

信頼性のあるテストアプリやテストデータを用意できないと、十中八九モジュールは爆発する――私の職業プログラマとしての経歴の大半は、モジュールの開発に費やされてきたのだが、その中で悟ったことだ。

振り返ると、何らかのアプリケーションなりシステムなりを丸ごと自分で担当した、という機会は少ない。それよりも、そこそこ大きなシステムの一部を開発した、という経験の方が圧倒的に多い。今もそうである。

だからクラスやコンポーネントなど――便宜上、以降ではモジュールと表現する――を実装して、他人に使用してもらったり、時には自身で組み込み先システムにモジュールを呼び出すコードを追加したりする、というスタイルが身についている。

ところで、コーディングしたモジュールは、それ単体では動作しない。動かすためには、組み込み先となる何かを用意しなくてはならない。

開発中のアプリにべったりのモジュールなら、恐る恐るアプリに組み込んで動かすし、独立性の高いモジュールなら、別途テスト用のアプリケーションを用意することになる。

この時、ままありがちなことだが、「信頼性が担保されていないモジュール」を「信頼性に乏しいソフトウェア」に組み込んで動かした場合、何か問題があった時に「モジュール」と「組み込み先のソフトウェア」のどちらに問題があるのか、もしくは双方に問題があるのか、原因の切り分けで苦労することになる。

だから、もしもテスト用のアプリケーションを別途用意することが許される環境であるならば、テストアプリの実装の際には、モジュールを書いた時と同等ぐらいに品質に気を配るべきだ。

もちろん、テストアプリだからこその手抜きは許されるだろう。ただしこの場合の「手抜き」には、「テストアプリの基本的な品質は担保されている」という前提がある。この前提を忘れてはならない。つまり、手抜きが許される部分と許されない部分がある、ということだ。

一般には、テストアプリ実装時に手抜きが許されるのはフールプルーフだろう。利用者が開発者本人ないし同等レベルのプログラマであることもあり、フールプルーフの機構を組み込まずに「運用でカバー」というやり方をしても許される余地がある。

また「無くても困らないが、あれば便利」という機能も、「実装のコスト」と「テスト省力化の期待値」との兼ね合いで省略されることが多い。例えばユーザが入力したパラメータの保存/読み出し機能などは、テストの省力化が期待されるなら実装されるが、それほど省力化に繋がらないなら見送られるだろう。

コードを書けば書くほど、潜在的なバグの数は増えるだろう。その観点では、テストアプリの機能をスリム化することは、テストアプリ自身の品質向上に結びつくはずだ。この点より「無くても困らないが、あれば便利」な機能の実装は見送られやすい、という面もある。

一方で内部品質に関しては、例えテストアプリであっても高品質が求められる。これこそが「テストアプリの基本的な品質は担保されている」という前提をもたらす。仮に内部品質が悪ければ、テストアプリは安定動作せず、モジュール組み込み後に「動作が不安定である原因はモジュールにあるのか、それともテストアプリなのか?」と悩むことになるだろう。

テストアプリに関して言えば、ベテランの言う「手抜きアプリだから」は「内部品質には気を使っているけど、フールプルーフとか便利機能とかはあまり考えていないから」という意味となる。そこだけは、次世代にしっかり伝えておくべきだ。さもなくば後進は内部品質まで怪しいテストアプリを書いてしまうだろう。

人はなぜPCを自作していたのだろうか?(Re: 自作PCってなんで人離れていったんだろ)

自作PCってなんで人離れていったんだろ

興味深い疑問である。

そもそも人はなぜPCを自作していたのだろうか? この疑問に思いをはせる際に、人々の「PCにたいする姿勢」に注目すると、ほんの少しだけ見えてくるものがある。

  1. PCはあくまで手段である、という人たち
  2. PCそのものが目的化している人たち

1990年代半ばから2000年代前半までは、PCはまだまだ高価だった。そのため「PCはあくまで手段である、という人たち」の中でも「道具にこだわる」タイプの人の一部には「安価で性能のよいPC」を求めて自作PCに手を出す人がいた。当時は自作PCの方が安く済む余地がそれなりにあったのだ。

PC自作までたどり着かなくても、メーカー製PCにたいして、例えばメモリが足りないとか、内蔵のグラフィックチップではCRTディスプレイの最大解像度にてフルカラーを選択できないとか、そういう問題を解消するためにサードパーティのPCパーツを購入して取り付ける人もそれなりにいた。PC自体の基本性能がまだまだ低かった時代で、メーカー製のPCにてその手の不満が起きることもあったのだ。

で、メーカー製PCをカスタマイズしたり自作PCに手を出したりしていた人の一部が沼に落ちる――という流れで「PCそのものが目的化している人たち」への人口流入が発生していた*1

  1. PCはあくまで手段である、という人たち
    1. 道具にこだわる系の一部 → PCを自作 → 一部は(2)に鞍替え
    2. それ以外 → PCを購入
  2. PCそのものが目的化している人たち
    • (1)からの人口流入が発生

付け加えると、2000年代前半ぐらいまでは「汎用性のあるコンピュータを持ち運ぶ」ということは常識ではなかった。ノートPCはまだまだ高価でかつ非力だったので*2、デスクトップPCやタワーPC*3が主流だった上に、まだまだブラウン管が使われたCRTディスプレイも多かった*4PDAの類はコモディティ化しておらず、どちらかといえばマニアのオモチャに近かった*5。そしてスマートフォンはまだ存在しなかった*6

PCは設置して使うもので、まだまだ筐体の大きなデスクトップPC/タワーPCが多かった。「物理的にも拡張性が高いPC」が占める割合が高かったこともあり、サードパーティから様々なPCパーツが出回っていた。

そんな環境で、PC単体が高価だったこともあり、「PCそのものが目的化している人たち」の多くは「PCパーツ」を軸としてPCを弄ることが多かった。それはPC自作だけでなく、メーカー製PCのカスタマイズ*7も含む世界だった。

  1. PCはあくまで手段である、という人たち
    1. 道具にこだわる系の一部 → PCを自作 → 一部は(2)に鞍替え
    2. それ以外 → PCを購入
  2. PCそのものが目的化している人たち
    • (1)からの人口流入が発生
    • PCパーツが軸 → メーカー製PCのカスタマイズ and PC自作

さて、2000年代半ばになると、PCがコモディティしたこともあり、価格は随分と安くなった。またPCの基本性能の向上により、普通にメーカー製の出来合いのPCをそのまま使っても、日常的な作業に支障がでることは減ってきた。

こうなると「PCはあくまで手段である、という人たち」にとってPCを自作するメリットは薄れる訳で、彼らは普通にメーカー製のPCを買うようになった。スリムタワーPCやコンパクトPCの割合が増えて「PCパーツによるカスタマイズ」の物理的な難易度が高くなったこともあり、カスタマイズのためのPCパーツを買う人も少なくなった。

コンシューマ向けの「PCパーツ」は以前ほど売れなくなった。市場の縮小によるメーカーの淘汰や、「売れるPCパーツへの『選択と集中』」によってマニアックなPCパーツが出回らなくなる、などの変化が発生した要因の1つではないかと妄想している。

PCに手を加えたり自作したりする機会が激減したことで、「PCそのものが目的化している人たち」への人口流入も減少したように思う。

  1. PCはあくまで手段である、という人たち
    1. 道具にこだわる系の一部 → PCを購入(※ただしスペック等は吟味する)
    2. それ以外 → PCを購入
  2. PCそのものが目的化している人たち
    • (1)からの人口流入が減少

加えてノートPCの性能向上と低価格化が進んだことや、後年のスマホタブレット端末の普及もあり、汎用性のあるコンピュータを持ち運ぶことが一般的になった。この影響でPCは「拡張の余地がある未完成品」ではなく「すでに完成したもの」である、という認識がより一般化したように思う(持ち運び易さや、場合によっては「防水・防塵」を考えると、完成品である方が都合がよい)。実際、当時のノートPCで弄りやすかったのは「メモリモジュールの交換」ぐらいで、拡張ボードが挿せないために「メモリモジュールの交換」の次は「内蔵ディスクの交換」と一気に難易度が高くなった*8

「PCは完成品である」という認識が普及したこともあり、「PCそのものが目的化している人たち」の中にも「PCパーツ」ではなく「PC単品」を愛でる人が出てくるなど、ある種の多様性が生じたように思う。いるでしょう、ノートPCや安鯖に特化した人が。

現在は「PCそのものが目的化している人たち≒PC自作スキー」が成立しない時代だ。

外界の影響もある。2000年代後半には「『完成品』としてのPC」の部分的な代替となるスマホタブレット端末が登場した。同じころに「『拡張の余地がある未完成品』としてのPC」の代替にもなりうるシングルボードコンピュータBeagleBoardが登場したし、2010年代半ばにはRaspberry Piが発売されてヒットした。「PCそのものが目的化している人たち」の中には「実はコンピュータそのものが目的化している」というケースもあり、そういう人たちはPCという形態にはこだわらない(かつては他に選択肢がなかっただけだ)。

あとPC-UNIXとかそっち寄りの場合、その昔はOSのインストールで苦労しないように「厳選されたパーツ」でPC自作していたパターンもあるのだが、今となってはVirtualBoxのような仮想環境もあれば、クラウドLinuxインスタンスもある訳で、そっち方面に解脱しているケースもある。*9

そんなこんなで、時の流れによってPC自作以外に軸足を移したロートルはそれなりにいると思う。

  1. PCはあくまで手段である、という人たち
    1. 道具にこだわる系の一部 → PCを購入(※ただしスペック等は吟味する)
    2. それ以外 → PCを購入
  2. PCそのものが目的化している人たち
    • (1)からの人口流入が減少
    • 「PC自作派」以外の派閥が形成されている
    • 実は「コンピュータそのものが目的化」 → SBCなど「PC以外の選択肢」がある

現状は、PC自作への人口流入が少なく、PC自作以外への人口流出が起きている状態だろう。人口流入と人口流出のどちらが多いのか、明確なエビデンスを持ち合わせていないのだが、個人的には「新規参入が少ない中、環境の変化によってロートルがPC自作を止めていっている」という印象がある。

これは根拠のない妄言だが、1990年代後半から2000年前後は「Windows 95/98の登場によるPCの爆発的普及」と「非力なメーカー製PCをサードパーティのPCパーツでカスタマイズしやすかった時期」と「PC自作で『安価で高性能なPC』を構築できた時期」が偶然にも重なった、いわば「PC自作人口のベビーブーム」といえる時代だったように思う。

現在40歳代前半の人たちは、当時は10代後半から20代前後という「流行の影響を受けやすく、かつ年齢的にバイト等で収入を得ることが不可能ではなかった」年齢だった。つまり「PC自作にハマる」ための下準備が整いやすい環境下にいたといえる。

あと、団塊ジュニアほどではないものの、それなりに出生人口が多い世代でもある。なので、仮に「人口あたりの『若い時にPC自作にハマる人』の割合」が全世代でほぼ同じだったとすると、今の若年世代と比較すれば「パイ全体が大きい」訳で、要するに40歳代前半は「PC自作人口ピラミッドボリュームゾーン」だった可能性がある。

この年代は、私を含めて氷河期世代な訳で、結婚している人は家庭の事情(空間的にも金銭的にも)で、未婚の場合でも経済的事情などで、PC自作から撤退する契機が多々ある。そして経済的事情と晩婚化の組み合わせは、昨今の「中高年のバイクブーム」の類似としての「PC自作への復帰」が後年に生じる可能性を氷河期世代から奪っているように思う。

  1. PCはあくまで手段である、という人たち
    1. 道具にこだわる系の一部 → PCを購入(※ただしスペック等は吟味する)
    2. それ以外 → PCを購入
  2. PCそのものが目的化している人たち
    • (1)からの人口流入が減少
    • 「PC自作派」以外の派閥が形成されている
    • 実は「コンピュータそのものが目的化」 → SBCなど「PC以外の選択肢」がある
    • 家庭の事情による「ボリュームゾーン」のロートルの撤退?

ところで、ここ数年のPC自作といえばゲーミングPCだが、これまでの分類を当てはめるならば、ゲーミングPCは「PCはあくまで手段である、という人たち」の世界の産物だと思う。今は諸事情により自作している人もいる状況だが、この傾向がどこまで続くのか、どこかの時点で「メーカー製のゲーミングPCで十分だよね」という転換点を迎えるのか、少し興味がある。

PCパーツを買う人が減れば、コンシューマ向けの流通が減り、パーツ購入が難しくなり、結果として私がPCパーツを買うのに苦労することになる――という個人的なワガママより、PC自作の今後の動向を暇な時に注視していきたい。

……さてと、いい加減そろそろ2年前にパーツを揃えた「積み自作PC」を消化しないとなあ。それと、勢いだけで買って持て余しているJ5005-ITXをどうするかも決めないと。

*1:当然ながら、それ以外の経路で「PCそのものが目的化」した人たちもいたことに留意すること。

*2:ノートPCの普及については、2003年にIntelPentium Mを発売して、それを搭載したノートPCが出回ったあたりで、潮目が変わったように思う。2006年発売のモバイル向けCore 2 Duoによって、CPUに起因する性能面の不満は(普段使いの範囲では)ほぼ解消された感じだった。

*3:もっともタワーPCについては、少なくとも1990年代末には「フルタワーはデカすぎる、せめてミドルタワーじゃないと」という認識だったように思う。なおここ1~2年ぐらいのゲーミングPC向けタワー型ケースを1990年代末のミドルタワー型のケースと比較すると、高さは概ね同じくらいだが、幅と奥行きは今のケースの方が大きくて、そのおかげでメンテナンス性が向上している。

*4:メーカー製PCに液晶ディスプレイが付属することが一般化したのは2000年を過ぎたぐらいの時期だったと記憶している。

*5:……まあ、持っていたんだけどね。SONYCLIE。仕事でも使ってた。

*6:なおガラケーも中身はコンピュータであるが、アレはどちらかといえばワープロのような専用機で「汎用性のあるコンピュータ」という感覚はなかった

*7:メモリを増設したり、拡張ボードを挿したり、オーバードライブプロセッサに手を出したり……。

*8:主に「OSを含むデータ移行」という面で……。

*9:あえてハードウェアの存在にこだわる場合も、Macという「BSD流のユーザランドを完備した、吊るしのコンピュータ」があるからなあ。