読者です 読者をやめる 読者になる 読者になる

第二魚雷発射管発射管姫路だいじょうぶじゃないでぃす

この記事は まんがタイムきらら Advent Calendar 2016 - Adventar の10日目の記事です。

www.adventar.org

間に合いませんでした。

第二魚雷発射管姫路さんが掃海時になぜ助かったのかの話をしたかったのですが間に合いませんでした。

下はお茶を濁すための姫路さんです。

煮たり焼いたりしてください。

f:id:Ryooooooga:20161218103219p:plain

それはそうと

はいふりといえば登場するキャラがみんなかわいいことで有名ですね。

私は第二魚雷発射管の姫路さんがすきです。

魚雷発射管とは

念のため補足しておくと、魚雷発射管は魚雷をしゅぽしゅぽ打ち出すあれです。

晴風には 61cm4連装魚雷発射管 が船体の中央付近に前後 2基 装備されています。*1

それぞれ前側の第一魚雷発射管を りっちゃん(松永 理都子) が、 後ろ側の第二魚雷発射管を かよちゃん こと 姫路 果代子さん が担当していて、 水雷長のメイちゃん(西崎 芽依)の指示で魚雷を発射します。

人物

ちっちゃい。

かわいい。

相撲に詳しい。

あほ毛。

主な登場シーン

魚雷の発射担当という役割上、対比叡戦、対アドミラル・グラフ・シュペー第2戦、対武蔵戦では大活躍だった(はず)のですが画面上にはほとんど登場しませんでした。

(対さるしま戦ではりっちゃんが魚雷(訓練弾)を発射、対シュペー第1戦、対伊201戦では魚雷の残弾が無かったため水雷戦は無し)

また、商店街船「しんばし」の海難救助にはダイバーとして作戦に参加し、被害状況の確認と浸水区画の捜索を担当していました。

そして、姫路さんを語る上で外せないのが機雷掃海作戦です。

機雷掃海作戦

あらまし

機雷の敷設された海域に迷い込んでしまった晴風、安全に後悔航海するために周囲の掃海を行う必要があります。

副長「人力の水中処分は危険だ」
艦長スキッパーを使おう」
ココ「あれなら小さいので機雷に引っかかる可能性は低いです」
タマ「あん……ぜん*2
艦長「スキッパー乗員には重安全具の装着を」

というわけで晴風の搭載している中型スキッパーを用いた掃海が行われることになりました。

スキッパーの操縦には免許が必要であり、特に中型スキッパー免許の取得には小型スキッパー免許の一年以上の保有が最低条件になります。*3

晴風乗員で中型スキッパー免許を持っているのは艦長、和住 媛萌(ヒメちゃん)ぞな子サトちゃん(勝田 聡子)、そして りっちゃん の4名のみです。*4

そんなこんなで りっちゃん と 姫路さん の二人がスキッパーでの掃海具の曳航を行うことになりました。*5

顛末

結果としてりっちゃんの操縦するスキッパーは係維機雷を支持するケーブルを高々数本*6切断したところで機雷に接触[要出典]し、ふっとばされることと相成りました。

重安全具

さて、はいふり世界のブルマー人はこの現実世界の人間よりも比較的丈夫であることが知られています。

下にその根拠となる幾つかの例を挙げます。

ということで、頑強なブルマー人(の卵)は基本的に半袖セーラーでなんでもこなしてしまいます。

とはいえ鋼鉄の艦艇を攻撃することを目的とした機雷を相手取るにはいくらなんでもそれだけでは無理があるようで、上の会話の通り重安全具の装着が指示されます。

というわけで以下の画像が重安全具を装備した姫路さん(と りっちゃん)です。

f:id:Ryooooooga:20161218000847j:plain

姫路さん(不安げな表情がかわいい) の手首についているのが恐らくそれです。

本来はこの重安全具の話がしたかったのですが間に合いませんでした。

ともあれ、機雷の至近爆発に巻き込まれた 姫路さんら は重安全具のお陰[要出典]で軽症で済んだのでした。

まとめ

はいふり世界の人間は丈夫。

姫路さんはかわいい。

ハイスクール・フリート ファンブックを買って。

www.hai-furi.com

*1:ハイスクール・フリート ファンブック P110

*2:危険

*3:ハイスクール・フリート ファンブック P114

*4:ハイスクール・フリート ファンブック P114

*5:二人乗りのため 姫路さん は免許がなくても問題はない

*6:画面で確認できるのは少なくとも2本

*7:ハイスクール・フリート ファンブック P114

篤見唯子のスロウスタート

この記事は まんがタイムきらら Advent Calendar 2016 - Adventar の10日目の記事です。

www.adventar.org

はじめに

今年は今までで最も多くの まんがタイムきらら の作品に触れることの出来た一年でした。

その中でも最も私の好きな作品の一つ、篤見唯子先生の「スロウスタート」を紹介したいと思います。

可能な限りネタバレなどは避けます。

作品について

スロウスタート は 篤見唯子先生 によって まんがタイムきらら 2013年7月号より連載されている作品です。

単行本は3巻まで発売されています。(2016年12月現在)

淡く優しいふんわりとした色使いの特徴的な絵柄がかわいらしいです。

登場人物

※ 単行本内のイラストを貼り付けるのがはばかられたので画像はありません。1巻のAmazonプレビュー、3ページ目あたりを見ながらだとキャラクタの想像がし易いと思います。

主な登場人物をざっくり紹介します。

いち花名はな

主人公。

高校一年生。実家を離れて従姉のおんの管理するアパートで一人暮らしをしている。

人見知りでネガティブ思考、泣き虫で打たれ弱い豆腐メンタル。

みんなには言えない秘密がある。

かわいい。

くら

高校一年生。ヘアピンがトレードマーク。

花名のクラスメイトで人気者、圧倒的コミュ強。天然たらし。

かわいい。

ももたまて

高校一年生。ツインテールの元気っ娘。

栄依子とは同じ中学出身。

重度のオタク。

八重歯。かわいい。

せんごくかむり

高校一年生。ちっちゃくてマイペース。

栄依子とは小学校の同級生。栄依子によく懐いている。

かわいい。

魅力

スロウスタートは言うなれば、花名が自身の抱えた秘密と向き合う物語です。

友人たちと過ごす何気ない日常の中で、時折ふっと鎌首をもたげるそれとどう折り合いをつけるのか。

秘密を打ち明けることで何かが変わってしまうかもしれないという不安と、親しくしてくれる友だちにそれを隠し続けることへの罪悪感。

けれども周囲の優しさの中でゆっくりと成長する花名の姿こそがこの作品の魅了だと思うのです。

かなり短くなってしまいましたが、文字通りいい意味でスロウな作品なのでこれ以上踏み込んだ話をしようとするとどうしてもネタバレを含んでしまうのでこのあたりで。

蛇足ではありますが、あんまり短すぎるのも申し訳ないので拙作の花名とたまてのデフォルメアイコンを投げておきます。酔狂な方が居ましたらTwitterのアイコンなどにご自由にどうぞ。

f:id:Ryooooooga:20161209220312p:plain:w300f:id:Ryooooooga:20161209225600p:plain:w300

C++17 optionalの実装について

C++17で、optionalが標準ライブラリに入ることが決定した。

そして、C++17 optionalはconstexprに対応している。

以前からその実装について気になっていたので調べたついでに少しまとめる。

optionalとは

無効値を取ることのできる型である。

C++17 optionalはBoost.Optionalの実装を元に設計された。

Boost.Optionalに関しては以下の記事が詳しい。

d.hatena.ne.jp

C++03 Boost.Optionalの実装

C++03時代のBoost.Optionalの実装についても触れておく。

C++03 Boost.Optionalの実装は単純だ。

予めoptionalオブジェクト内にデータを格納するストレージを確保しておき、
適切なタイミングでplacement newをして、初期化する。

また、オブジェクトを破棄する際には、必要があれば明示的デストラクタ呼び出しを行う。

ストレージの確保には、(C++11以降でいうところの)std::aligned_storageのようなものを用いる。

つまり、optionalの内部で領域の動的確保は行われない。

下記のコードはBoost.Optionalの実装のイメージだ(故に正確ではない)。

template <typename T>
class optional {
    //  storage
    typename std::aligned_storage<sizeof(T), alignof(T)>::type storage_;
    bool init_;
    
    //  pointer to storage
    T* ptr() {
        return static_cast<T*>(static_cast<void*>(&storage_));
    }
    
public:
    // default constructor
    optional() : init_(false) {}
    
    // value constructor
    optional(const T& val) : init_(true) {
        //  placement new
        new (&storage_) T(val);
    }
    
    //  destructor
    ~optional() {
        if (init_) {
            // explicit destructor call
            ptr()->~T();
        }
    }
};

見て分かる通り、この実装のoptionalクラスはtrivialデストラクタを持たない。

更には引数付きコンストラクタはconstexprコンストラクタの条件を満たさない。

なので、このままではconstexpr対応の条件であるリテラル型クラスの条件を満たさない。

C++17 optionalはどのような実装により、constexpr対応を果たしたのだろうか。

その前に

上のコードでは、Tの型に関わらずoptional<T>型はデストラクタで明示的デストラクタ呼び出しを行っているが、
実際には、Tがtrivialデストラクタをもつ場合は明示的デストラクタ呼び出しを省略できる。

そのため、Tがtrivialデストラクタを持つときには、optional型もまたtrivially destructibleとなれる。

実装について

さて、ようやくC++17 optionalの実装を確認する。

constexpr対応のoptionalの実装は以下のリポジトリのものを参考にした。

github.com

だが、実際のソースコードは一息に読むには長いので、部分部分に分解して見ていく。

まずは、optional<T>型の定義を確認する。

定義は上記のリポジトリ、optional.hppファイルの350行目付近である。

// 350行目付近
template <class T>
class optional : private OptionalBase<T>
{
   ...
};

optional<T>型はOptionalBase<T>型を継承している。

OptionalBase<T>型はそのすぐ上で定義されている。

// 341行目付近
template <class T>
using OptionalBase = typename std::conditional<
    is_trivially_destructible<T>::value,
    constexpr_optional_base<typename std::remove_const<T>::type>,
    optional_base<typename std::remove_const<T>::type>
>::type;

std::conditional<B, X, Y>::typeは、B == trueの時 X型になり、B == falseの時 Y型となる。

ようは、条件分岐のようなものだ。

この場合の分岐条件は、

is_trivially_destructible<T>::value

つまり、Tがtrivialデストラクタを持つかどうかだ。

Tがtrivialデストラクタを持つ時、OptionalBase<T>は、constexpr_optional_base<T>となり、
Tが非trivialデストラクタを持つ時、OptionalBase<T>は、optional_base<T>となる。

双方の定義を確認する。

// 296行目付近
template <class T>
struct optional_base
{
    bool init_;
    storage_t<T> storage_;

    ...

    ~optional_base() { if (init_) storage_.value_.T::~T(); }
};

// 319行目付近
template <class T>
struct constexpr_optional_base
{
    bool init_;
    constexpr_storage_t<T> storage_;

    ...

    ~constexpr_optional_base() = default;
};

「...」で省略した部分はどちらのクラスもほぼ同じであった。

違いは2点。

1つ目は、storage_ メンバ変数の型、

2つ目は、デストラクタでのメンバの明示的デストラクタ呼び出しの有無だ。

constexpr_optional_baseは、Tがtrivialデストラクタを持つため、明示的デストラクタ呼び出しを省略しているのだろうと予想できる。

では、storage_t と constexpr_storage_t の定義を確認する。

// 266行目付近
template <class T>
union storage_t
{
  unsigned char dummy_;
  T value_;

  ...

  ~storage_t(){}
};

// 281行目付近
template <class T>
union constexpr_storage_t
{
    unsigned char dummy_;
    T value_;

    ...

    ~constexpr_storage_t() = default;
};

このoptionalは、ストレージにstd::aligned_storageのような型を使わず、共用体を使って実現しているようだ。

storage_t と constexpr_storage_t の定義の違いは、デストラクタの定義のみである。

ではこの違いは何故必要なのか。

これには、C++11の「共用体の制限解除」が関係している。

C++11以降では非trivialデストラクタを持つオブジェクトも共用体のメンバにすることが可能になった。

しかし、その時その共用体のデストラクタは暗黙的にdeleteされる。

そのため、非trivialデストラクタを持つT型のメンバ value_ を持つstorage_tには空のデストラクタ定義が必要になる。

さらに、共用体のコンストラクタの初期化子を使って value_ を初期化すれば、T型のコンストラクタが呼び出されるため、
optional<T>のコンストラクタ内での placement new の必要がなくなる。

以上をもって、Tがリテラル型である場合に、constexpr_storage_t<T>、constexpr_optional_base<T>、optional<T>の全てがリテラル型の条件を満たし、コンパイル時のオブジェクト構築が可能となる。

#include <type_traits>
#include <string>
#include <cassert>

template <typename T>
union storage_t {
	char dummy_;
	T value_;

	template <typename... Args>
	constexpr storage_t(Args&&... args) : value_(args...) {}

	~storage_t() {}
};

template <typename T>
union constexpr_storage_t {
	char dummy_;
	T value_;

	template <typename... Args>
	constexpr constexpr_storage_t(Args&&... args) : value_(args...) {}

	~constexpr_storage_t() =default;
};

template <typename T>
struct optional_base {
	bool init_;
	storage_t<T> storage_;
    
	template <typename... Args>
	constexpr optional_base(Args&&... args)
		: init_(true), storage_(args...) {}

	~optional_base() { if (init_) storage_.value_.~T(); }
};

template <typename T>
struct constexpr_optional_base {
	bool init_;
	constexpr_storage_t<T> storage_;

	template <typename... Args>
	constexpr constexpr_optional_base(Args&&... args)
		: init_(true), storage_(args...) {}

	~constexpr_optional_base() =default;
};

template <typename T>
using OptionalBase = std::conditional_t<
	std::is_trivially_destructible<T>::value,
	constexpr_optional_base<T>,
	optional_base<T>
>;

template <typename T>
class optional : OptionalBase<T> {
public:
	constexpr optional(const T& value) : OptionalBase<T>(value) {}

	constexpr const T& operator*() const {
		return OptionalBase<T>::storage_.value_;
	}
};

int main() {
    // constexpr
    constexpr optional<int> a(42);
    static_assert(*a == 42);
    
    // 非constexpr
    /*constexpr*/ optional<std::string> b("nyan");
    assert(*b == "nyan");
}

コンマ

ふと不思議に思ったものがあった。
線形代数ライブラリEigenの行列、ベクトルの初期化方法だ。

Eigen::Vector3d v;
v << 1.0, 2.0, 3.0;
v;  //  (1, 2, 3)

なんだか妙なことをしている。
どのようなしくみになっているのかを確かめるために実装を調べた。

どうやら コンマ演算子オーバーロードしているようだ。
はじめに <<演算子 で CommaInitializer なる中間オブジェクトを生成し、CommaInitializer の コンマ演算子オーバーロード で値を格納しているらしい。

恥ずかしながら今まで コンマ演算子オーバーロード可能であることを知らなかった。
そういえば、オーバーロード可能演算子に含まれている。

まあせっかくなので少し遊んでみたい。

#include <iostream>
using namespace std;

namespace hoge {
    template <typename Value>
    ostream& operator,(ostream& stream, Value value) {
        return stream << ',' << value;
    }
}

int main() {
    cout << 1,2,3,4,5;   // 1
    cout << endl;
    using namespace hoge;
    cout << 1,2,3,4,5;   // 1,2,3,4,5
    cout << endl;
}

出力

1
1,2,3,4,5

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

さて、この記事を書いている途中、コンマ演算子オーバーロード用いた Boost.Assign なるものがあることを教えていただいた。

#include <boost/assign.hpp>

int main() {
    using namespace boost::assign;
    std::vector<int> v;

    v += 1,2,3,4,5; // [1,2,3,4,5]
}

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ
内部は、Eigen のそれと似たようなものなのだろう。
Boost.Assign ではこれ以外にも演算子オーバーロードされているようだ。

このコードにはバグがある

今日はこのようなツイートを見た。


#include <stdio.h>

class base_class {
public:
    base_class() { x = 0; y = 0; }

public:
    int x;
    int y;
};

class derived_class : public base_class {
public:
    derived_class() { z = 0; }

public:
    int z;
};

void calc_class_members(base_class *b, int array_size)
{
    for(int i = 0; i < array_size; i++)
    {
        b[i].x = i;
        b[i].y = i;
    }
}

// Calling from external
int update_class_members(
    derived_class * class_array,    // Valid array
    int num_array )
{
    if( class_array == NULL )
        return -1;    // means error

    calc_class_members(class_array, num_array);

    return 0;// success
}

このコードの誤りは base_class と derived_class の大きさの違いによるものであるが、取り敢えず実行してみれば明らかである。

int main()
{
    constexpr int num_array = 5;
    derived_class class_array[num_array];

    update_class_members(class_array, num_array);
    
    for(auto&& a : class_array)
        printf("%d, %d, %d\n", a.x, a.y, a.z);
    
    puts("---");
    
    printf("%p, %p, %p\n", &class_array[0]
                         , &class_array[1]
                         , &class_array[2]);
    
    printf("%p, %p, %p\n", &static_cast<base_class*>(class_array)[0]
                         , &static_cast<base_class*>(class_array)[1]
                         , &static_cast<base_class*>(class_array)[2]);
}

実行結果
http://melpon.org/wandbox/permlink/mVYWFiM7eEieWFeE

0, 0, 1
1, 2, 2
3, 3, 4
4, 0, 0
0, 0, 0
---
0x7ffd2e5e31e0, 0x7ffd2e5e31ec, 0x7ffd2e5e31f8
0x7ffd2e5e31e0, 0x7ffd2e5e31e8, 0x7ffd2e5e31f0

この通り、derived_class は base_class よりも sizeof(int) だけ大きいため、
配列の先頭要素へのポインタ class_array を base_class* へとキャストした後に添字演算を行うとずれた位置にアクセスしてしまう。

問題の答えたるバグはおそらくこれのことで間違いないだろう。

ではこのプログラムが正しい動作をするように書き直したい。

まず思いついたのは calc_class_members を template化 することであるが、
それだけでは元々のプログラムの意図が損なわれてしまう。

そのため、template の型をチェックして、base_class の派生クラスでないものは弾いてしまおう。


だがそれ以前に、このコードは生ポインタを使っていたりととてもモダンなC++とは呼べない。なので、ついでに適当に手直しをしておく。

//#include <stdio.h> // そもそもいらない
#include <array>
#include <vector>

struct base_class {
    int x = 0;
    int y = 0;
};

struct derived_class : base_class {
    int z = 0;
};

template <typename type, typename allocator>
void calc_class_members(std::vector<type, allocator>& b)
{
    static_assert(std::is_base_of<base_class, type>::value, "");

    int i = 0;
    for(type& a : b)
    {
        a.x = i;
        a.y = i;

        i++;
    }
}

template <typename allocator>
void update_class_members(std::vector<derived_class, allocator>& class_array)
{
    // nullptr チェックの必要はない
    
    calc_class_members(class_array);
}


// あるいはこうだろうか
template <typename type, size_t n>
void calc_class_members(std::array<type, n>& b)
{
    static_assert(std::is_base_of<base_class, type>::value, "");

    int i = 0;
    for(type& a : b)
    {
        a.x = i;
        a.y = i;

        i++;
    }
}

template <size_t n>
void update_class_members(std::array<derived_class, n>& class_array)
{
    calc_class_members(class_array);
}
#include <iostream>

int main()
{
    auto class_array1 = std::vector<derived_class>{ 5 };
    auto class_array2 = std::array<derived_class, 5>{};

    update_class_members(class_array1);
    update_class_members(class_array2);

    std::cout << "class_array1" << std::endl;
    for(auto&& a : class_array1)
        std::cout << a.x << ", " << a.y << ", " << a.z << std::endl;

    std::cout << "class_array2" << std::endl;
    for(auto&& a : class_array2)
        std::cout << a.x << ", " << a.y << ", " << a.z << std::endl;
}

実行結果
http://melpon.org/wandbox/permlink/COZ8R4QW2OXFM6u8

class_array1
0, 0, 0
1, 1, 0
2, 2, 0
3, 3, 0
4, 4, 0
class_array2
0, 0, 0
1, 1, 0
2, 2, 0
3, 3, 0
4, 4, 0

お見かけした他の方のコード


なるほど、
こんな手は思いつきもしなかった。

文字列型をキーとしたunordered_map

プログラミングをしている際、文字列型をキーとした連想配列を扱いたい時がある。

そのような時に用いられるのは多くの場合unordered_mapだ。

using Map = unordered_map<string, int>;

Map m = {
	{ "a", 1 },
	{ "b", 2 },
	{ "c", 3 },
};
m["a"]; // 1
m["b"]; // 2

ところで、unordered_map の []演算子 の引数は (const key_type&) あるいは (key_type&&) である。

そして、今 key_type は std::string だ。

そのため、

m["a"];

m.operator[](string{"a"});

と同義だ。

なんでもないアクセスのために std::string のインスタンスが生成されている。そして、std::string のコンストラクタでは "a" という文字列を格納するためのバッファが確保される。バッファの確保は大抵の場合比較的遅い。

なにか良い方法はないか。


さて、boost::string_ref と言うものが存在する。詳細な説明は省略するが、要は文字列を格納したバッファへの ポインタ と 文字数 のみを保持するだけのクラスである。

string_ref は内部でポインタを保持するだけで、一切のバッファ確保を行わない。

つまり、こいつをキーに出来れば上のような問題は起きないのだ。

そして勿論、 string_ref 等のような独自定義の型であっても、==演算子オーバーロード(あるいはコンパレータ)とハッシュ値を求めるハッシャさえ存在すれば、unordered_mapのキーとして使える。

#include <boost/utility/string_ref.hpp>
#include <boost/functional/hash.hpp> // boost::hash_range

using namespace boost;

struct Hasher {
	size_t operator()(string_ref str) const {
		return hash_range(str.cbegin(), str.cend());
	}
};

using Map = unordered_map<string_ref, int, Hasher>;

Map m = {
	{ "a", 1 },
	{ "b", 2 },
	{ "c", 3 },
};
// バッファの確保は起こらない
m["a"]; // 1
m["b"]; // 2

一見これで良さそうだ。

だが、これにもまだ問題がある。

先程も述べたように、string_ref はあくまでバッファへのポインタを保持するクラスである。つまり、文字列の本体はどこかに存在していなくてはならないのだ。

そのため、

int main() {
	Map m;

	{
		string str = "###";
		m[str] = 42;
	}// もはやstrは存在しない

	m["###"]; // ?
}

このようなコードは未定義動作となる。

何故ならば、m["###"]; が実行されるときにはもうバッファの本体たる str は破壊されているからだ。


今必要なのは、キーを登録する際には std::string のようにバッファを確保し、ただアクセスするときには boost::string_ref のようにバッファを確保せず、比較とハッシュ値の算出のみを行う型である。

では、愚直に実装してみる。

#include <boost/optional.hpp>

using namespace boost;

class Key {
	optional<string> str;
	string_ref       ref;

public:
	Key(string_ref s, bool flag = false) {
		if(flag) {
			str = s.to_string();
			ref = *str;
		}
		else {
			ref = s;
		}
	}

	Key(const Key& key)
		: str(key.str), ref(str ? *str : key.ref) {}

	Key(Key&& key)
		: str(std::move(key.str)), ref(str ? *str : key.ref) {}

	bool operator==(const Key& key) const {
		return ref == key.ref;
	}

	string_ref str() const {
		return ref;
	}
};

struct Hasher {
	size_t operator()(const Key& key) const {
		return hash_range(key.str().cbegin(), key.str().cend());
	}
};

boost::optional を用いてバッファの有効/無効を表現した。

また、コンストラクタの第二引数のフラグのon/offによってバッファを確保するかどうかを分岐させている。

実際にキーとして使用するときには以下のようになる。

using Map = unordered_map<Key, int, Hasher>;

Map m = {
	{ Key{"a", true}, 1 },
	{ Key{"b", true}, 2 },
	{ Key{"c", true}, 3 },
};

m[{"a"}]; // 1

string_ref str = "b";
m[str]; // 2   暗黙の型変換

……………………。

テキトーに時間計測を行ってみた結果、確かに、string をキーとした時よりも、速かった。とは言えそこまで大した差ではない。

ぶっちゃけこんなことするくらいならばもっと別の箇所を見直したほうがいいような気がした。

v8を崇めよ

映画を見に行った。

今年頭の「楽園追放」ぶりの映画館だ。

お目当ては「ジュラシック・ワールド」、Twitterで某氏がご満悦のようであったので気になっていたのだ。

予約は済ませた。そして、映画館にたどり着いた。

 

――上映時間を間違えた。20:40からの上映にもかかわらず18:50着。我ながらどう勘違いしたのか分からない。そこでふとチケット売り場の上映情報を見た。「2D字幕・マッドマックス 19:05」。

そもそも今日はマッドマックスを見るために映画館に行くはずだったのだが、マッドマックスのレイトショーでの上演は22:00以降であったため、マッドマックスは次の機会に、取り敢えず本日のところは興味のあったジュラシック・ワールドを見ておこうと思ったのだ。

19:05は通常料金ではあるがまあ差分は大した出費ではない。チケットを購入した。

 

マッドマックスは全席ペアシートであった。

一人で悠々と、広い座席を専有してくつろぎつつ上映を待った。

 

 

本編開始2分から上映時間の7000秒間、口を閉じることすら忘れひたすらにv8エンジンに脳汁を絞られ続けた。

v8エンジンを崇めよ。