syghの新フラグメント置き場

プログラミングTipsやコード断片の保管場所です。お絵描きもときどき載せます。

ログ用に時刻を取得して文字列化する (C#, Java, POSIX/Win32)

アプリケーションのデバッグや、デプロイ後の問題解析に最も重要な役割を果たすのはログです。ログの機能や精度・粒度によって、問題解決のしやすさがほぼすべて決まります。また、ログは単なるトラブルシューティングだけでなく、場合によってはユーザー操作記録などの簡易的なエビデンス(証拠)としても使えます*1
通例、動作ログにタイミング情報を記録する際、レコード行に時刻情報を含めることで、状態遷移の正しさの確認や速度性能解析に役立てることができます。いわゆるprintfデバッグです。レコードの時刻情報はログをファイルに記録するときだけでなく、デバッグコンソールに出力するときにも役立ちます。

また、秒単位ではなくミリ秒単位の時刻情報が記録できると性能解析に便利です*2。なお、時刻は協定世界時 (UTC) で記録しておくと逆シリアライズが簡単になるというメリットがあるのですが、現地時刻(ローカル時刻)とタイムゾーン情報(GMTからのオフセット)を記録する方式のほうが、人間が直接ログレコードを見て判断しやすくなるというメリットがあります。

ログファイルのフォーマットはパフォーマンスの観点から、ファイルの全書き換えではなくプレーンテキストの末尾追加で済むCSVフォーマットやTSVフォーマットなどが好ましいです*3。また、後で回収できるように、日付ごとにファイルを分けて作成したり、直近数か月分を保持して古いものは自動削除する循環ログにしたり、といった機能を実装します。自分はロギング機能を自前で実装したことが何度かありますが、通例 log4jlog4net を使っているプロジェクトも多いのではないかと思います。

今回はロギングの実装までは踏み込まず、C#, Java, C/C++におけるそれぞれの時刻情報取得と文字列化(フォーマット)についてのみ説明します。
ちなみにISO 8601は日付と時刻の間にTを入れるルールになっているのですが、今回は見やすさ優先のためASCII空白0x20にしました。

C#

C# (.NET) は後発なだけあって一番簡単です。洗練されていて美しいですね。余計な解説をするようなことはほとんどありません。

using System;

public class DateTimeFormatTest {
    public static void Main() {
        const string format = "yyyy-MM-dd HH:mm:ss.fffzzz";
        // GMT からのオフセット値が 0 の場合、タイムゾーンは "+00:00" になる。 
        Console.WriteLine("Local = " + DateTime.Now.ToString(format));
        Console.WriteLine("UTC   = " + DateTime.UtcNow.ToString(format));
    }
}

もしローカル時刻とUTC時刻との間の変換が必要な場合、TimeZoneInfo.ConvertTimeFromUtc()TimeZoneInfo.ConvertTimeToUtc()を使います。

なお、ラウンドトリップ書式指定子 "O", "o" を用いることで、簡単にISO 8601形式の書式化(シリアライズ)と解析(逆シリアライズ)ができるようです。

Java

Javaも時刻関連ライブラリは最初期から実装・標準化されています。しかしタイムゾーンの扱いが若干厄介な印象を受けます。ログのパーサーを書くときに考慮する必要があります。
注意点として、SimpleDateFormatはスレッドセーフではないので、スレッド間でインスタンスを使いまわすのは厳禁です。スレッドごとにインスタンスを作成する必要があります。

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;

public class Main {
    private static final String FORMAT = "yyyy-MM-dd HH:mm:ss.SSSXXX";
    public static void main(String[] args) {
        // GMT からのオフセット値が 0 の場合、タイムゾーンは "Z" になる。 
        System.out.println("Local = " + (new SimpleDateFormat(FORMAT, Locale.ROOT).format(new Date())));
        final SimpleDateFormat sdf = new SimpleDateFormat(FORMAT, Locale.ROOT);
        sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
        System.out.println("UTC   = " + sdf.format(new Date()));
    }
}

C/C++

.NETもJavaもプラットフォーム非依存の高水準環境であり、標準ライブラリも充実しているので特に困るようなことはありませんが、問題はC/C++です。こいつらは標準化がとにかく遅い言語で、時刻関連の標準ライブラリは貧弱の一言です。まともな情報を取得するには、プラットフォームごとのライブラリやAPIを使わざるを得ません。

POSIX

UNIX時刻を表すtime_t型からtm構造体*4への変換がミソです。ちなみに逆変換をする関数mktime(), timegm()も存在します。

なお、標準Cライブラリのlocaltime(), gmtime()はスレッドセーフでないので使わないようにします。代わりにリエントラントなlocaltime_r(), gmtime_r()を使います。

UNIX系OSの場合、通例time_t型は32bit OSでは32bit整数、64bit OSでは64bit整数で実装されていることが多いため、32bit UNIX2038年問題を回避できません。2038年は32bit UNIXが死ぬ年です。

ミリ秒単位あるいはそれよりも高分解能で時刻を得たい場合は、gettimeofday()timevalの値を取得するか、またはclock_gettime(CLOCK_REALTIME)timespecの値を取得します。
CLOCK_REALTIMEはシステム時刻の変更や、NTPによる同期の影響を受けるため、いわゆるカレンダー時刻を取得するのに使えますが、基本的に2点間の経過時間計測には使えません。2点間の経過時間計測にはCLOCK_MONOTONICCLOCK_MONOTONIC_RAWを使うべきです。また、マイクロ秒単位やナノ秒単位での記録が不要な場合、かつLinux環境では、CLOCK_REALTIME_COARSECLOCK_MONOTONIC_COARSEを使うとパフォーマンスが向上するそうです。

下記コード例で使用しているtm構造体のメンバーのうち、タイムゾーン関連のtm_gmtofftm_zoneはC標準ではなく、BSD/GNU拡張になっています。そのため環境によっては使えない可能性もあります。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
//#include <sys/time.h>

int main() {
    printf("sizeof(time_t) = %d\n", static_cast<int>(sizeof(time_t)));
#if 0
    // 秒単位まででよければ、C 標準の time() 関数を使う。
    const time_t now = time(nullptr);
#elif 0
    // C 標準では time_t の実際の型が整数であるかどうかは規定されていない。
    // POSIX では符号付き整数であることが保証される。
    time_t now = 0;
    if (time(&now) == -1) {
        puts("Failed to get time!!");
        return -1;
    }
#elif 0
    // gettimeofday() を使うと、マイクロ秒単位で時刻を求めることができる。
    // timezone 構造体は廃止予定の非推奨機能。
    // gettimeofday() 関数は廃止予定の非推奨機能。
    struct timeval tvo = {};
    //struct timezone tzo = {};
    //gettimeofday(&tvo, &tzo);
    gettimeofday(&tvo, nullptr);
    const time_t now = tvo.tv_sec;
#else
    // clock_gettime() を使うと、ナノ秒単位で時刻を求めることができる。
    struct timespec tso = {};
    if (clock_gettime(CLOCK_REALTIME, &tso) != 0) {
        puts("Failed to get clock time!!");
        return -1;
    }
    const time_t now = tso.tv_sec;
#endif

    // ctime_r() や asctime_r() は出力フォーマットがお粗末なので却下。
    struct tm tmo = {};
    if (localtime_r(&now, &tmo) != nullptr) {
        // tm_gmtoff, tm_zone は BSD/GNU 拡張。
        // tm_gmtoff は GMT (UTC) からのオフセットで、秒単位。
        const int gmtOffsetHourPart = tmo.tm_gmtoff / 3600;
        const int gmtOffsetMinPart = abs(tmo.tm_gmtoff / 60) % 60;
        // ミリ秒部分を浮動小数点数で計算して単純に "%03.0f" 書式で四捨五入すると、例えば 999.9 の繰り上がりで破たんする。
        //const int msPart = static_cast<int>(tvo.tv_usec / 1000);
        const int msPart = static_cast<int>(tso.tv_nsec / 1000L / 1000L);
        printf("Local = %04d-%02d-%02d %02d:%02d:%02d.%03d%+03d:%02d\n",
            tmo.tm_year + 1900, tmo.tm_mon + 1, tmo.tm_mday, tmo.tm_hour, tmo.tm_min, tmo.tm_sec,
            msPart,
            gmtOffsetHourPart, gmtOffsetMinPart);
        // DST = Daylight Saving Time: 夏時間(サマータイム)。
        printf("WeekDay = %d, YearDay = %d, DST = %d, Zone = \"%s\"\n",
            tmo.tm_wday, tmo.tm_yday, tmo.tm_isdst, tmo.tm_zone);
        // +hhmm や -hhmm 形式でよければ strftime() を使うこともできる。
        char buf[128] = {};
        strftime(buf, sizeof(buf), "%F %T %z (%Z)", &tmo);
        printf("Local = %s\n", buf);
    }

    if (gmtime_r(&now, &tmo) != nullptr) {
        printf("UTC   = %04d-%02d-%02d %02d:%02d:%02d\n",
            tmo.tm_year + 1900, tmo.tm_mon + 1, tmo.tm_mday, tmo.tm_hour, tmo.tm_min, tmo.tm_sec);
    }
}

Win32

Visual C++のランタイムライブラリには、POSIXlocaltime_r(), gmtime_r()に相当するlocaltime_s(), gmtime_s()がMSVC 8.0以降のSecure CRTとして実装されていますが、引数や戻り値の仕様が異なるので注意が必要です。また、C11規格(C++11ではない)ではlocaltime_s(), gmtime_s()が追加されていますが、MSVCの同名関数とはインターフェイスが異なるので注意が必要です。ちなみに、MSVC 8.0以降のtime_tは64bit化されていて、32bitプラットフォームでも既定で64bit整数となり、2038年問題を回避できるようになっています*5

いずれにせよ、Windowsにはgettimeofday()clock_gettime()が実装されておらず、tm構造体のtm_gmtoff, tm_zoneメンバーも実装されていないので、ミリ秒単位で時刻を求めたり、タイムゾーン情報を取得したりする場合はWindows APIを直接叩くのがベストだと思います。

なお、TIME_ZONE_INFORMATION構造体のメンバーのうち、夏時間の扱いはよく分かりませんでした。日本はサマータイムとかいうクソシステムなんぞ採用していないのに、DaylightBias-60となります。
このメンバーはGetTimeZoneInformation()関数の戻り値がTIME_ZONE_ID_DAYLIGHTであるときのみ使われるということでしょうか。

This value is added to the value of the Bias member to form the bias used during daylight saving time. In most time zones, the value of this member is –60.

標準Cライブラリが第一級市民扱いとなっているUNIXと違い、Windowsでは第一級市民ではなく、標準C/C++ライブラリはすべてWin32 API上に構築されています。Win32では最初から時刻表現が64bit化されているので、32bitアプリケーションでも2038年問題を回避できる仕様になっています*6

#include <stdio.h>
#include <stdlib.h>
#include <locale.h>
#include <Windows.h>

int main() {
    setlocale(LC_ALL, "");

    SYSTEMTIME loc = {};
    ::GetLocalTime(&loc);
    SYSTEMTIME utc = {};
    ::GetSystemTime(&utc);

    TIME_ZONE_INFORMATION tzInfo = {};
    const DWORD timeZoneId = ::GetTimeZoneInformation(&tzInfo);
    printf("TimeZoneId = %lu\n", timeZoneId);
    if (timeZoneId != TIME_ZONE_ID_INVALID) {
        // TIME_ZONE_INFORMATION::Bias は分単位。
        const int gmtOffsetHourPart = -tzInfo.Bias / 60;
        const int gmtOffsetMinPart = abs(tzInfo.Bias) % 60;
        printf("Local = %04hd-%02hd-%02hd %02hd:%02hd:%02hd.%03hd%+03d:%02d\n",
            loc.wYear, loc.wMonth, loc.wDay, loc.wHour, loc.wMinute, loc.wSecond,
            loc.wMilliseconds,
            gmtOffsetHourPart, gmtOffsetMinPart);
        printf("WeekDay = %hd, Zone = \"%S\"\n", loc.wDayOfWeek, tzInfo.StandardName);
    }
    printf("UTC   = %04hd-%02hd-%02hd %02hd:%02hd:%02hd.%03hd\n",
        utc.wYear, utc.wMonth, utc.wDay, utc.wHour, utc.wMinute, utc.wSecond,
        utc.wMilliseconds);
}

説明の簡略化のためprintf系を使いましたが、C/C++で文字列バッファにフォーマット出力するときはsnprintf系やstd::stringstreamを使います。Boost.Formatを使うのもひとつの手です。もしWindows限定でよければATL::CString::Format()を使うと簡単です。

Boost C++ライブラリのboost::posix_timeboost::date_timeを使えば、一応プラットフォーム非依存なコードを書けそうですが、JavaC#のように言語標準の方法で手軽かつ簡単に、というわけにはいきません。

ところで、C++11では時刻を扱うstd::chronoが追加されましたが、このライブラリは設計が大仰なわりに機能が貧弱で、しかも使い方が直感的でなく、かなり面倒です。C++20で日付フォーマット関連の機能が追加されるらしいのですが、今更ですね。標準化が遅すぎる。C/C++JavaC#に比べて言語機能もライブラリも10年どころか20年以上遅れをとっています。先発言語の事例を参考にして十分に練り込み、利便性の高いものを出してくるのであればそれでも我慢できるかもしれませんが、標準化委員会の連中は時代に逆行したユーザビリティ最悪のオナニーツールしか作れないようです。生産性や移植性の低いC/C++は、21世紀における新時代のアプリケーション開発に使うべき言語ではありません。引退すべき言語とまでは言いませんが、今後はハードウェア層に近いバックエンドの記述などにとどめるべきです。

*1:もちろんコンプライアンス上の問題がない限り、という条件は付きます。ログにユーザーIDやパスワードを平文で記録するようなことは絶対にしてはいけません。最近の事例だとFacebookがやらかしていたことが報じられています。Facebookが数億件分のパスワードを暗号化しないままサーバーに保存していたことが明らかに - GIGAZINE

*2:システム時刻を処理時間計測に使うと、アプリケーション稼働中にシステム時刻が変更された場合に破たんするので、単調増加することが保証されるmonotonic (steady) なタイマーを使って2点間の差を計算するのがセオリーです。とはいえ、稼働中にシステム時刻の変更がないという前提であれば、システム時刻を処理時間計測に使うことはできます。

*3:XMLJSONは階層構造やオブジェクトリストの表現には向いていますが、ファイルの先頭と末尾にタグやブレースの開始/終了ペアが必要であり、追記モードでファイルを開いて新しいレコードだけを後から追加するようなことができません。

*4:名前が完全に意味不明ですね。Cのライブラリは歴史的にひどい略語のシンボルが多く、可読性が最悪です。

*5:逆に言うと、古いコンパイラコンパイル&ビルドされたアプリケーションは2038年問題を回避できません。

*6:よくWindowsPOSIXに準拠していないことを批判されるのですが、そういう連中はPOSIX仕様のお粗末さを理解していません。まともにアプリケーション開発をしたことがないんでしょう。POSIXは全体的に時代遅れの遺物です。

Android Javaのアサーション

昨年2018年は転職したこともあって、一番よく使った言語はJavaでした。とはいっても、ほとんどはAndroidJavaであり、サーバーサイドやデスクトップで使われている正式なJavaとはいささか性質が異なります。

JavaC/C++C#と違ってプリプロセッサをサポートしないのですが、そのせいで一番困るもののうちのひとつはアサーション(表明)です。アサーションは「契約による設計」(design by contract) を実現・実践するために有効なプログラミング手法であり、呼び出し元が保証すべき事前条件や、呼び出し先が保証すべき事後条件をコード中に記述し、条件が満たされなかった場合はプログラムを続行せずに中断します。一方、リリースビルド(本番環境)でアサーションが無効になっているときは、引数として渡した式(表明したい条件)が評価されずに実行コストゼロとなることが好ましく、そのためC/C++C#ではコンパイルオプションに応じて有効/無効が切り替わるプリプロセッサマクロやConditional属性付きメソッドによって、ゼロオーバーヘッドのアサーションを実現しています。逆に言うと、リリースビルドではアサーションの条件式は評価されないため、必ず実行されないと困るような式(システムに対して副作用のある式)は記述しないようにします。

#include <cassert>
// NDEBUG シンボルが定義されていない場合、assert マクロが有効になる。
// アサーションが有効な場合は assert マクロが実行される。無効な場合は引数の評価ごと無視される。
// 条件が満たされず、アサーションが失敗したとき、abort 関数が実行される。
static void test(const int ary[], size_t length) {
    assert(ary != nullptr);
    for (size_t i = 0; i < length; ++i) {
        printf("%d\n", ary[i]);
    }
}

C/C++標準のassert()マクロは、表明が失敗 (assertion failure) するとabort()関数でプログラムを強制終了しますが、C# (.NET) のDebug.Assert()メソッドは表明が失敗した場合に無視してプログラムを続行することもできます。MSVCのCRTマクロ、_ASSERT()_ASSERTE()に近い挙動です。

// DEBUG シンボルが定義されている場合、Assert メソッドが有効になる。
// アサーションが有効な場合は Assert メソッドが実行される。無効な場合は引数の評価ごと無視される。
// 条件が満たされず、アサーションが失敗したとき、エラーメッセージが表示される。
static void Test(int[] ary)
{
    System.Diagnostics.Debug.Assert(ary != null);
    foreach (int x in ary)
    {
        System.Console.WriteLine(x);
    }
}    

Javaにはバージョン1.4で assert ステートメントが追加されたのですが、Javaアサーションコンパイルオプションではなく実行時オプションで有効/無効を切り替えます。JITコンパイラによる最適化がうまく機能すれば、無効時にはゼロコストとなることが期待できます。

// アサーションが有効な場合は assert ステートメントが実行される。無効な場合は引数の評価ごと無視される。
// 条件が満たされず、アサーションが失敗したとき、java.lang.AssertionError がスローされる。
static void test(int[] ary) {
    assert ary != null;
    for (final int x : ary) {
        System.out.printf("%d\n", x);
    }
}

上記の例に限れば、仮にリリースビルドでnullが渡されたとしても、途中でjava.lang.NullPointerExceptionがスローされることでプログラムは中断されますが、nullを有効値として許可しないメソッドであるということをコード上で明らかにするためにもアサーションは有用です。Java 7以降であれば、java.util.Objects.requireNonNull()を併用するとなお良いです。ちなみにJSR 305の@Nonnullアノテーション (javax.annotation.Nonnull) はメソッドインターフェイスのドキュメント化に役立ちますが、すべての処理系で使えるわけではないのが残念です。

Android Javaの場合

しかし、Android 4.xまでの仮想マシン Dalvik では、アサーションの有効/無効を切り替えるシステム プロパティの debug.assert は信頼性がなく(無視されることがある)、さらにAndroid 5.0以降の仮想マシン ART (Android Runtime) では未実装とのこと。
Android StudioJava言語組み込みのassert文を使おうとすると、以下のような静的チェックツール (lint) による警告が出ます。

Assertions are not checked at runtime. There are ways to request that they be used by Dalvik (`adb shell setprop debug.assert 1`), but note that this is not implemented in ART (the newer runtime), and even in Dalvik the property is ignored in many places and can not be relied upon.
Instead, perform conditional checking inside `if (BuildConfig.DEBUG) { }` blocks. That constant is a static final boolean which is true in debug builds and false in release builds, and the Java compiler completely removes all code inside the if-body from the app.
For example, you can replace `assert speed > 0` with `if (BuildConfig.DEBUG && !(speed > 0)) { throw new AssertionError() }`.
(Note: This lint check does not flag assertions purely asserting nullness or non-nullness; these are typically more intended for tools usage than runtime checks.)
https://code.google.com/p/android/issues/detail?id=65183

つまり現在のAndroidでは、Java言語組み込みのassert文はまともに使用できません。
代替として、上記 lint では java.lang.AssertionError を明示的にスローするコードを書くことが提案されていますが、正直そんなまどろっこしいことやってられません*1

Android開発環境が自動生成するクラスBuildConfig*2のboolean定数フィールドBuildConfig.DEBUGを使って分岐すれば、確かにコンパイラ最適化によりリリースビルドではゼロコストとなることが期待できるのですが、分岐&例外スローを逐一書くのがダルいので、自分はJUnitライクのアサーションユーティリティクラスと静的メソッドを作って対処しています。個人的には分岐やメソッド呼び出しのオーバーヘッドよりも、分岐&例外スローを逐一書くことでノイズとメンテナンスコストが増大することのほうが懸念だからです。なお、表明したい式を直接記述してbooleanで受け取るのではなく、ラムダ式で表現して遅延評価することにより、リリースビルドでの実行時コストを削減する方法もありますが、ラムダ式を使わない場合と比べてやはりコード上のノイズが増えます。

import java.util.function.*;

public class MyAssert {
    public static final boolean DEBUG = true; // BuildConfig.DEBUG で初期化する方式でも OK。

    public static void assertTrue(boolean condition) {
        if (DEBUG && !condition) {
            throw new AssertionError();
        }
    }

    public static void assertTrue(BooleanSupplier expression) {
        if (DEBUG && !expression.getAsBoolean()) {
            throw new AssertionError();
        }
    }
}

// 利用例。
MyAssert.assertTrue(ary != null);
MyAssert.assertTrue(() -> ary != null);

AndroidJavaは本物のJavaではなくJavaモドキでしかない、としばしば揶揄されますが、こういった非互換な仕様や動作には実際かなりウンザリさせられます。Oracleがブチ切れて訴訟を起こしたくなる気持ちも理解できます。自分にとってC/C++の一番嫌いなところは、処理系依存の仕様や未定義動作があまりに多すぎることですが、本来のJavaC#はそういった曖昧な仕様が極力排除されていることが最大の魅力のうちのひとつとなっています。仮にもJavaを名乗るのであれば、そういった根本的な設計思想を曲げるようなことをすべきではありません。これは技術者の倫理 (engineering ethics) に関わる問題です。

*1:ちなみにこの警告文中に記載されている例示コードですが、文末のセミコロンが欠けており、コンパイルが通りません。

*2:ちなみにBuildConfigクラスはアプリモジュールのパッケージ(名前空間)になるので、ライブラリモジュールからは直接参照することができません。

C++でstaticクラス

C#には「staticメンバーだけを定義して、実体化(インスタンス化)は許可しない」ようなクラスを簡潔に定義する手段があります。

public static class MyHelper
{
  public static int Add(int x, int y)
  { return x + y; }
}

staticクラスは状態を管理しない純粋ヘルパー(アルゴリズムや完全定数)を定義するのに便利です。代表的な例でいうと、System.Mathクラスが挙げられます。
なお、staticクラスからはサブクラスを派生させることもできません。

ちなみにJavaでもクラスをstaticキーワードで修飾することができますが、意味合いがまったく違います*1C#のほうがキーワードに即していて直感的だとは思いますが。

C++/CLIおよびC++/CXでは以下のように書くことでC#のstaticクラス相当を記述できます。

public ref class MyHelper abstract sealed
{
public:
  static int Add(int x, int y)
  { return x + y; }
};

しかし、従来の標準C++では、staticクラスを記述する文法は直接サポートされていません*2
せいぜいできることと言えば、以下のようにデフォルトコンストラクタとデストラクタを隠ぺいして実体化を禁止するくらいしかなく、C#と比べて分かりやすさはともかく美しさに欠けます。
また、C++03以前では構文上で派生を禁止することができず、派生を禁止するにはC++11以降のfinalキーワードを使う以外ありません。

// C++03 以前。
class MyHelper
{
private:
  MyHelper() {}
  ~MyHelper() {}
public:
  static int Add(int x, int y)
  { return x + y; }
};

なお、C++11以降ではデフォルトコンストラクタとデストラクタをdelete指定することで、実体化を禁止することもできます。メンバーをdelete指定する場合はprivateではなくpublicとするのがセオリーです。

// C++11 以降。
class MyHelper final
{
public:
  MyHelper() = delete;
  ~MyHelper() = delete;
public:
  static int Add(int x, int y)
  { return x + y; }
};

MSVCにはネイティブC++でもabstractキーワードを使えるように言語拡張が施されているので、abstract sealedまたはabstract finalを指定することでstaticクラスを記述することができるのですが、Clangなどでは使えません。移植性の高いコードを書きたい場合は注意が必要です。MSVCではコンパイルオプション/permissive-を付けることで独自拡張を無効化することもできます。

2023-05-07追記:
C++17以前では言語仕様にお粗末なバグがあり、コンストラクタをdelete指定しているにもかかわらず集成体初期化による実体化ができてしまいます。前述のようにデストラクタもdelete指定しておけば、少なくとも削除はできなくなるので大抵のケースでは実質的に実体化を禁止できますが、さらにコンストラクタをexplicit指定しておけばC++17以前でも確実に実体化を禁止できます。C++20以降ではこの仕様バグが修正されています。

class MyHelper final
{
public:
  explicit MyHelper() = delete;
...
};

*1:Javaのクラスに対するstatic指定子は、入れ子になったネストクラスにのみ適用可能です。staticを指定しないネストクラスは外部クラスのインスタンスを暗黙的にキャプチャするインナークラス(内部クラス)となり、メソッド内で定義可能な匿名クラスやローカルクラスと似たようなクロージャ特性を持ちます。staticを指定したネストクラスは外部クラスのインスタンスをキャプチャしない静的クラスとなり、C#のネストクラスに近くなりますが、ネストクラスのメンバーに対するアクセス指定子の扱いがC#とは異なります。個人的にJavaのネストクラスは内部クラス/静的クラス問わず直感性に欠け、落とし穴の多い欠陥機能だと思っています。Nested Classes (The Java™ Tutorials > Learning the Java Language > Classes and Objects)

*2:C++では必ずしもクラスに関数を定義する必要はなく、クラスに属さない名前空間レベルの関数を定義することもできるので、これまでstaticクラスのサポートが重要視されてこなかったのかもしれません。

無修正画像のリクエストを受けたとき、どう対処・回答すべきか

三次元のお話

自分には縁のない話ですが、一般論として。
恋人に「裸の自撮り写真を送ってくれ」「他の誰にも見せたりしないから」などとお願いされても、はっきりと拒絶すべきですね。そしてそんなことを臆面もなく言ってくるリテラシーの低い野郎とは即刻別れるべきです。将来表を歩けなくなってもかまわないなら話は別ですが。
つい忘れがちですが、インターネットは全世界につながっています。一見プライバシーが保護されているように見えるメールやSNSのDMも例外ではありません。いまや画像はもちろん動画でさえ、誰でも簡単にデータをアップロード・共有できますが、同時にクリックひとつで全世界に公開できてしまいます。ネット上に裸の画像をばらまかれて永久に消せない汚点を残すような、いわゆるリベンジポルノにつながるだけでなく、刑法175条のわいせつ物頒布罪(わいせつ電磁的記録記録媒体頒布罪*1)に問われる可能性もあります。

二次元のお話

では、二次元のイラストや漫画に関してはどうでしょうか?
やはり日本国内では三次元同様「無修正はわいせつ物とみなされてアウト」になります。
pixivなどのSNSサイトで、R18カテゴリーのエッチなイラストや漫画をアップロードされている方は注意が必要です(自分を含めて)。
自分で撮った写真や自分で描いた絵などを、自分だけがアクセスできる個人端末のハードディスクや光学メディアに保存すること、つまり無修正の元データの単純所持自体には法律的に問題ありません。そうでないと作品の創作や制作ができませんので。もちろん児童ポルノは除きます。児童ポルノは無修正だろうが修正済みだろうが、単純所持ですらアウトです*2児童ポルノ、ゼッタイダメ。

しかし、たとえ自作であろうとも、無修正画像を不特定多数に公開・譲渡するとなると、前述の刑法175条に抵触します。公開対象の多い少ないにかかわらず、メールやSNSのファイル添付、ストレージサービスなど、共有目的でインターネットにアップロードすること自体が危険だと考えてください*3
某ポルノサイトでは海外のサーバーに無修正データをアップロードすることで日本の法律を潜り抜けようとしていますが、限りなく黒に近いグレーだということもお忘れなく。

よって、他人から「○○の無修正画像をもらえませんか?」などというリクエストを受けても、安易に譲渡してはいけません。

個人的意見

これは絵描きとしての個人的な意見ですが、せっかく描いたものに、モザイクやぼかし、ベタ/ホワイトで修正なんてしたくないんです。同人誌を発行するときや、SNSサイトに公開するときに、ときおり修正が弱いと指摘されることがあり、しぶしぶ手直ししていますが、内心では引っかかるものがあります。別にゾーニングができていればいいじゃないか。むやみに隠そうとするから逆におかしなことになるんです。むしろ修正したほうが余計にわいせつ感が増すと感じるのは自分だけではないはず。滑稽ですね。ゾーニングができていない(年齢詐称による抜け道があり、事実上誰でも閲覧できる)ことのほうがよほど問題です。

具体的に何をどの程度修正すれば「わいせつ物」とみなされないか、に関しても、法律で明確に定められているわけではありません。警察のさじ加減ひとつで決まってしまいます。修正は出版社・印刷会社・イベント主催者や創作者の自主規制でしかありません。でも捕まりたくはないので、安全側に自主規制せざるを得ない。ジレンマですね。その自主規制も年々強くなっているようで、業界全体が萎縮しているように感じられます。昔はゴールデンタイムに放送されるアニメでも乳首くらいは余裕だった*4というのに……

*1:「記録」が連呼されていますが、どうやら誤植ではなく法律上の正式名称らしいです。ややこしいですね。

*2:非実在青少年」とやらの話は、ここではツッコミません。あしからず。

*3:たとえ個人的なバックアップ目的だとしても、クラウドサーバーに無修正画像をアップロードするのは避けるべきです。

*4:昨今の深夜アニメの謎の光や自主規制マークは、円盤の購買意欲をそそるための差別化戦略でもあるのかもしれませんが。

Windows 10 1709/1803でキャレットの点滅が止まる

Windows 10 1709 (Fall Creators Update) には、エディットボックスなどのキャレット(縦棒のカーソル)の点滅が5秒程度で止まってしまうバグがあるようです。確かキャレットはシステムタイマーでOSが点滅させているはず*1ですが、フォーカスしてから5秒くらい経過すると点滅が止まってしまいます。キャレットに独自の形状を使って自前で描画しているMIFESでも点滅が停止します。1703 (Creators Update) 以前ではこんな現象は発生していませんでした。1803にも同様のバグがあるようです。

キャレットの点滅は、現在のフォーカスがどのUI要素にあるのか、またアプリ(のUIスレッド)が生きているのかどうかを知ることのできる重要なインジケーターのひとつなので、これが止まってしまうとかなり困ります。この問題は、MFCアプリを開発していたときに気づきました。一瞬アプリ側のバグもしくは設計ミスかとも思いましたが、ありとあらゆるWin32アプリで発生するのでOS側に問題があることは確実です。なおFirefoxは自前のタイマーでキャレットを点滅させているようで、OSのバージョンに関係なく点滅は無限に続きます。OneNote 2016は30秒くらい経過すると止まりました。

しかし、あろうことか窓の杜の某ライターは、キャレットの点滅が止まってしまうのを「仕様変更」だとうそぶいています。これは仕様変更じゃなくてただのお粗末なバグです。なぜ公式発表もないのに、れっきとしたメディアのライターがいい加減なことを書くのでしょうか。

さらに無理やり点滅を継続させるサードパーティ製ツールの紹介までしていますが、こんなくだらないツール作るヒマがあったらMSに不具合報告すべきですね。なんでも日曜大工でDIYすればいいってもんじゃないです。自動車の構造や計器に安全基準不適合の不具合や設計ミスがあったらリコール対象でしょう。不具合に気付いたら勝手に自分で改造して対応したり、その場しのぎの改造方法をユーザー間で共有したりするべきではなく、社会全体のことも考えて国土交通省に報告すべきです。ちなみにWindows 10ではエンドユーザーでもフィードバックHubで不具合報告できるので、おかしな動作に気がついたら個人でもどんどん報告すべきですが、多数の意見・要望が集まらない限りMSは動こうとしません。無視・黙殺されるのがオチです。

Windows 10の品質問題

Windows 10は特に従来のWin32デスクトップまわりの品質が明らかに低下しているように思われます。1709はGetPixel/SetPixel関数のパフォーマンスデグレードもやらかしやがったので、まったく良い印象がありません。1703は2018年10月でサポート期限が切れますが、1709はスキップして1803にすることも検討しています。
それにしても今のWindowsは、すでにある機能を破壊・劣化させてまで追加する価値のある機能など、なにひとつ提供できてないと思います。むしろ余計な新機能のほうがユーザーエクスペリエンスの邪魔をしているくらいです。Windows 7/8.1同様の快適性を得るために、いったいいくつの機能を無効化したか知れません。既存機能の破壊と劣化はApple同様、リリーススケジュールばかり優先して品質確保をおろそかにしているツケがまわってきているだけですが、頻繁な大型アップデートによる新機能追加なぞまったく望んでもいないユーザーが、なぜそこで不利益を被らなければならないのか。極めて疑問です。

*1:点滅間隔はOSのシステム設定で変更できるほか、SetCaretBlinkTime() APIが用意されています。

FirefoxでGoogle検索結果から特定のサイトを除外する

安全・快適なインターネットライフのために、Google検索結果から不快・不正なサイト(NAVERまとめとか)は除外したいですよね。

Firefox Quantumより前のバージョンでは、「Hide Unwanted Results of Google Search」というアドオンを使っていました。このアドオンの素晴らしいところは、ワイルドカード正規表現で除外したいサイト・ページを柔軟に指定することができた点です。しかし、Quantum (v58以降) では仕様変更により、古いアドオンが使えなくなりました。「Hide Unwanted Results」はその後更新されておらず、Quantumに対応していないので非常に残念です。

Quantumに更新した後、現在は「Personal Blocklist (not by Google)」を使っています。もともとGoogle Chrome向けの公式アドオンとして公開されていた「Personal Blocklist (by Google)」を、Firefox向けに書き直したものだそうです。

ただ、残念ながらこのアドオンは「Hide Unwanted Results」と違い、ドメイン/ホスト単位でしか除外できません。たとえばQiita (qiita.com) から特定のユーザー*1のページだけを除外する、ということはできないわけです。

Googleの検索設定もしくは検索オプションで除外指定できれば一番いいのですが、おそらくGoogleは広告収入を断たれたくないのでフィルタリングには消極的なのだと思われます。

*1:Qiita自体は有用で参考になりますが、他のユーザーをバカにした不快なコメントを連発している傲慢なユーザーや、コメント欄でたびたびケンカ・炎上させているようなユーザーのページは検索結果にすら表示させたくないです。

Windowsセキュリティ認証とネットワークパスワード

Windowsで共有フォルダーにアクセスする場合、セキュリティ認証のためのパスワード入力を求められることがあります*1

しかし、PCを再起動/シャットダウンするなどして、ログオンセッションが終了すると、再度入力が必要になってしまいます。

コマンドプロンプトでセキュリティ認証を実行する場合、以下のようにnetコマンドを使うことができます。

net use \\<IPアドレスもしくはホスト名>\<共有名> /user:<ユーザー名> <パスワード>

再入力の手間を省く方法として、コマンドをバッチファイルに書き込んでおいて、バッチファイルをスタートアップに登録する手法もあるのですが、パスワードを平文でテキストファイルに書き込むのはセキュリティ上のリスクがありますね。

代わりに「資格情報マネージャー」(Credential Manager) を利用するとよさそうです。

www.mtecweb.com

Windows 10の場合もコントロールパネルからアクセスできます。

ちなみにTortoiseGitなどもWindowsのセキュリティ認証を使用しますが、あろうことか認証ダイアログで誤ったパスワードを入力してしまった場合でも、その誤ったパスワードが資格情報マネージャーに記録されてしまいます。以後、認証ダイアログが出ずに接続が延々と失敗し続けてしまう現象が発生します。誤ったパスワードを削除する場合、コントロールパネルの資格情報マネージャーから各情報セットを削除する必要があります。

*1:同一ユーザー名で同一パスワードの場合、パスワード入力なしに透過的にアクセスできます。