1. 例外処理
著者:梅谷 武
語句:cons
語句:cons
各言語における例外処理の扱い方を調べ、それを使ってコマンド行解析の雛型を作る。
作成:2024-07-21
更新:2024-10-24
更新:2024-10-24
C++23の例外処理についてはstd::exceptionを参照のこと。C++23以前のC++標準からさらに改定されている。本稿で使うstd::logic_errorとstd::runtime_errorの継承関係をまとめておく。
- exception
- logic_error(論理エラー:実行前検出)
- invalid_argument(不正引数)
- domain_error(定義域外)
- length_error(データ長超過)
- out_of_range(値域外)
- future_error(スレッドライブラリから)
- runtime_error(実行時エラー)
- range_error(範囲外)
- overflow_error(オーバフロー)
- underflow_error(アンダーフロー)
- regex_error(正規表現ライブラリから)
- system_error(OSから)
- ios_base::failure(入出力ライブラリから)
- filesystem::filesystem_error(ファイルシステムから)
- tx_exception
- nonexistent_local_time
- ambiguous_local_time
- format_error(書式設定ライブラリから)
- logic_error(論理エラー:実行前検出)
OCamlの例外処理についてはThe OCaml Manual>I. An introduction to OCaml>Ch. 1 The core language>6 Exceptionsを参照のこと。
標準ライブラリで定義されている組み込み例外型については
The OCaml Manual>IV. The OCaml library>Ch. 28 The core library>1 Built-in types and predefined exceptionsを参照のこと。次にその一覧を記す。
- exception Match_failure of (string * int * int)
パターンマッチング失敗(ファイル名, 行番号, 列番号) - exception Assert_failure of (string * int * int)
assert失敗(ファイル名, 行番号, 列番号) - exception Invalid_argument of string
引数エラー(補足情報) - exception Failure of string
未定義エラー(補足情報)、stringのパターンマッチ非推奨。 - exception Not_found
対象が存在しない - exception Out_of_memory
メモリ不足(ガベージコレクションから) - exception Stack_overflow
スタック不足 - exception Sys_error of string
システムエラー(OSのI/O機能から)、stringのパターンマッチ非推奨。 - exception End_of_file
ファイル終端検出(OSのI/O機能から) - exception Division_by_zero
ゼロ除算 - exception Sys_blocked_io
システムエラー(非同期I/Oから) - exception Undefined_recursive_module of (string * int * int)
不完全な再帰モジュール定義(ファイル名, 行番号, 列番号)
Racketの例外処理についてはThe Racket Guide>10 Exceptions and Control>10.1 Exceptions、The Racket Reference>10 Control Flow>10.2 Exceptionsを参照のこと。
組み込み例外型exnについてはThe Racket Reference>10 Control Flow>10.2 Exceptions>10.2.5 Built-in Exception Typesを参照のこと。次にその階層構造を記す。
- exn
- exn:fail(エラー発生時)
- exn:fail:contract(実行時エラー)
- exn:fail:contract:arity(引数個数不正)
- exn:fail:contract:divide-by-zero(ゼロ除算)
- exn:fail:contract:non-fixnum-result(fixnum範囲外)
- exn:fail:contract:continuation(境界外ジャンプ)
- exn:fail:contract:variable(未定義変数)
- exn:fail:syntax(構文エラー)
- exn:fail:syntax:unbound(未束縛識別子)
- exn:fail:syntax:missing-module(モジュール不明)
- exn:fail:read(読み取りエラー)
- exn:fail:read:eof(EOF検出)
- exn:fail:read:non-char(特殊文字検出)
- exn:fail:filesystem(ファイルシステムエラー)
- exn:fail:filesystem:exists(既存)
- exn:fail:filesystem:version(版数不一致)
- exn:fail:filesystem:errno(エラーコード)
- exn:fail:filesystem:missing-module(モジュール不明)
- exn:fail:network(ネットワークエラー)
- exn:fail:network:errno(エラーコード)
- exn:fail:out-of-memory(メモリ不足)
- exn:fail:unsupported(未サポート)
- exn:fail:user(エンドユーザーエラー)
- exn:fail:contract(実行時エラー)
- exn:break(強制終了)
- exn:break:hang-up(ハングアップ)
- exn:break:terminate(終了要求)
- exn:fail(エラー発生時)
コマンド行から引数を与えて、それに応じた処理を行なうための雛型を作成する。例として自然数nを与えて階乗n!を計算するプログラムを書く。
long型を使って階乗を計算する。本環境ではlongは64ビット整数であり、値域は-9223372036854775808~9223372036854775807である。
factorial.cpp:
#include <limits>
#include <print>
#include <stdexcept>
#include <string>
long factorial(long n) {
if (n <= 0l) return 1l;
long result = 1l;
for (long i = 1l; i <= n; ++i) {
if (result > std::numeric_limits<long>::max() / i) {
throw std::overflow_error("Overflow detected.");
}
result *= i;
}
return result;
}
int main(int argc, char* argv[]) {
try {
if (argc != 2) {
throw std::invalid_argument(
"Usage: factorial <non-negative integer>");
}
long n = std::stol(argv[1]);
if (n < 0l) {
throw std::invalid_argument(
"The argument must be a non-negative integer.");
}
long result = factorial(n);
std::println("{}! = {}", n, result);
} catch (const std::logic_error& e) {
std::println(stderr, "Logic error: {}", e.what());
} catch (const std::runtime_error& e) {
std::println(stderr, "Runtime error: {}", e.what());
} catch (const std::exception& e) {
std::println(stderr, "Another std::exception: {}", e.what());
} catch (...) {
std::println(stderr, "Unknown exception.");
}
return 0;
}
makefile:
CPPG = g++
CPPL = clang++
CPPFLAGS = -std=c++23 -O2
SRC = factorial.cpp
EXES = factorial_g factorial_l
all: $(EXES)
factorial_g: $(SRC)
$(CPPG) $(CPPFLAGS) -o $@ $(SRC)
factorial_l: $(SRC)
$(CPPL) $(CPPFLAGS) -o $@ $(SRC)
clean:
rm -f $(EXES)
現時点でC++23対応は実験段階であり、コンパイルオプション:-std=c++23を付けなければならない。
実行するとn = 20まで計算できて、n = 21でオーバーフローする。
$ ./factorial_g 20
20! = 2432902008176640000
$ ./factorial_g 21
Runtime error: Overflow detected.
$ measure ./factorial_g 20
20! = 2432902008176640000
======================================
Process exited with status: 0
total time: 0.000000 [sec]
mem size: 3716 [KB]
code size: 119 [KB]
$ measure ./factorial_l 20
20! = 2432902008176640000
======================================
Process exited with status: 0
total time: 0.000000 [sec]
mem size: 3620 [KB]
code size: 93 [KB]
本環境のOCamlでは標準整数型intが64ビット整数になっているが、これはOSが64ビットの場合であり、OSが32ビットのときは32ビット整数になると言語仕様にある。
関数型言語では原則としてループを使わず再帰で表現するが、ここではC++と同じfor文で階乗を計算する。表示用にC言語のprintfと同じ書式が使えるPrintfモジュールを導入しておく。
factorial.ml:
open Printf
let factorial n =
if n <= 0 then 1
else
let result = ref 1 in
for i = 1 to n do
if !result > max_int / i then
failwith "Overflow detected."
else
result := !result * i
done;
!result
let () =
try
if Array.length Sys.argv <> 2 then
raise (Invalid_argument
"Usage: factorial <non-negative integer>")
else
let n = int_of_string Sys.argv.(1) in
if n < 0 then
raise (Invalid_argument
"The argument must be a non-negative integer.")
else
let result = factorial n in
printf "%d! = %d\n" n result
with
| Invalid_argument msg ->
eprintf "Logic error: %s\n" msg
| Failure msg ->
eprintf "Runtime error: %s\n" msg
| exn ->
eprintf "Another exception: %s\n" (Printexc.to_string exn)
OCamlにはocamlcとocamloptの二種類のコンパイラがある。前者はバイトコードコンパイラであり、後者はネイティブコードコンパイラである。本稿ではC++とOCamlの性能比較のため、OCamlのコンパイラとしてはocamloptを用いる。
makefile:
OCAMLOPT = ocamlopt
OCAMLFLAGS = -O2
LIBS =
SRC = factorial.ml
EXES = factorial
all: $(EXES)
factorial: $(SRC)
$(OCAMLOPT) $(OCAMLFLAGS) -o $@ $(LIBS) $(SRC)
clean:
rm -f $(EXES) *.o *.cmx *.cmi
実行するとn = 20まで計算できて、n = 21でオーバーフローする。
$ ./factorial 20
20! = 2432902008176640000
$ ./factorial 21
Runtime error: Overflow detected.
$ measure ./factorial 20
20! = 2432902008176640000
======================================
Process exited with status: 0
total time: 0.000000 [sec]
mem size: 2736 [KB]
code size: 1111 [KB]
Racketにおいて数(number)はexactかinexactのどちらかに分類される。exactな数とは任意精度整数か任意精度有理数であり、inexactな数とは浮動小数点表現された実数あるいは複素数である。整数計算においては特に指定しない限り任意精度計算が行われるため、通常は型を意識する必要がない。
factorial.rkt:
#lang racket
(require iso-printf)
(define (factorial n)
(if (<= n 0)
1
(let loop ([i 1] [result 1])
(if (> i n)
result
(loop (+ i 1) (* result i))))))
(define (main args)
(with-handlers
([exn:fail? (λ (e) (eprintf "%s\n" (exn-message e)))]
[exn? (λ (e) (eprintf "Unexpected: %s\n" (exn-message e)))])
(cond
[(not (= (vector-length args) 1))
(error "Usage: factorial <non-negative integer>")]
[else
(let ([n (string->number (vector-ref args 0))])
(cond
[(not (integer? n))
(error "The argument must be an integer.")]
[(< n 0)
(error "The argument must be a non-negative integer.")]
[else
(let ([result (factorial n)])
(printf "%d! = %d\n" n result))]))])))
(main (current-command-line-arguments))
表示用にC言語のISO標準互換のprintfを使うためにiso-printfパッケージを導入しておく。
$ raco pkg install iso-printf
Racketに付属するracoはバイトコードコンパイラであり、C++やOCamlのネイティブコードコンパイラと比べると性能面では劣る。
$ raco exe factorial.rkt
n = 21では任意精度計算をしているのでオーバーフローしない。
$ ./factorial 20
20! = 2432902008176640000
$ ./factorial 21
21! = 51090942171709440000
$ measure ./factorial 20
20! = 2432902008176640000
======================================
Process exited with status: 0
total time: 0.482103 [sec]
mem size: 133920 [KB]
code size: 12443 [KB]
This document is licensed under the MIT License.
Copyright (C) 2024 Takeshi Umetani