【覚え書き】Visual Studio CodeのVimExtentionでVimモードをデフォルトで無効にする。

設定でvim.disableExtensionをTrueにすれば、デフォルトでVim拡張が無効になる。

"vim.disableExtension": true

必要な時にControl+Shift+Pの”Toggle Vim mode”でVimモードとノーマルモードを切り替える。

github.com

VimからVSCodeにほぼ乗り換えたけど、時と場合によってはVimのほうが都合が良いので、VimExtentionを入れている。 ただ、これをインストールするとデフォルトでVIm拡張が有効になりVSCodeがほぼVimになってしまうので、これはよくない。 なので、Vimモードをデフォルトで無効にしたかった。

スマートポインタの使い方 その2:shared_ptr

shared_ptrを使ってみる

unique_ptrが変数の寿命が尽きた段階でメモリ領域を開放するのに対し、shared_ptrは参照カウンタを持ち参照カウンタがゼロになるとメモリ領域を開放する。

#include <iostream>
#include <memory>

// 円を表す図形
class CPrimitiveCircle {
 public:
  CPrimitiveCircle() : x(0), y(0), r(0) {}
  CPrimitiveCircle(int xx, int yy, int rr) : x(xx), y(yy), r(rr) {}
  int x, y, r;
};

int main(int argc, char* argv[]) {
  std::shared_ptr<CPrimitiveCircle> c1{
      std::make_shared<CPrimitiveCircle>(10, 20, 100)};

  // 振る舞いはあくまでもポインタ
  std::cout << "count = " << c1.use_count() << " r = " << c1->r << std::endl;

  std::shared_ptr<CPrimitiveCircle> c2 = c1;  // 参照回数が増える
  std::cout << "count = " << c1.use_count() << " r = " << c1->r << std::endl;

  std::shared_ptr<CPrimitiveCircle> c3 = c1;  // 参照回数が増える
  std::cout << "count = " << c3.use_count() << " r = " << c3->r << std::endl;

  return 0;
}

unique_ptrがstd::make_uniqueで生成するのに対し、shared_ptrはstd::make_shared関数で生成する。
コードの見た目はあくまでも「ポインタ」として振る舞うので、メンバのアクセスはアロー演算子(operator->)で行う。use_countメソッドはshared_ptrが管理しているメモリ領域の参照数を取得する。
比較も同様。operator==とoperator!=で管理しているメモリ領域が「同一」か否かもチェックできる。

生ポインタの取得と再初期化

生ポインタの取得はunique_ptrと同様getメソッドでshared_ptrが管理している領域の生ポインタを取得出来る。当然、このメモリ領域はdeleteしてはならない。同様に、resetメソッドで別のポインタで再初期化できる。unique_ptrとの違いは参照回数が減るだけで、別の同一のメモリ領域を参照しているshared_ptrそのものには影響が無い。

#include <iostream>
#include <memory>

class CPrimitiveCircle {
 public:
  CPrimitiveCircle() : x(0), y(0), r(0) {}
  CPrimitiveCircle(int xx, int yy, int rr) : x(xx), y(yy), r(rr) {}
  int x, y, r;
};

int main(int argc, char* argv[]) {
  std::shared_ptr<CPrimitiveCircle> c1{
      std::make_shared<CPrimitiveCircle>(10, 20, 100)};

  // 振る舞いはあくまでもポインタ
  std::cout << "count = " << c1.use_count() << " Addr = " << c1.get()
            << std::endl;

  std::shared_ptr<CPrimitiveCircle> c2 = c1;  // 参照回数が増える
  std::cout << "count = " << c1.use_count() << " Addr = " << c1.get()
            << std::endl;

  std::shared_ptr<CPrimitiveCircle> c3 = c1;  // 参照回数が増える
  std::cout << "count = " << c3.use_count() << " Addr = " << c3.get()
            << std::endl;  // ポインタは同値

  c1.reset(new CPrimitiveCircle(0, 0, 20));  // 別のポインタで初期化
  CPrimitiveCircle* pCircle = c1.get();
  std::cout << "count = " << c1.use_count() << " Addr = " << c1.get()
            << std::endl;
  std::cout << "count = " << c3.use_count() << " Addr = " << c3.get()
            << std::endl;  // 再初期化した分参照回数が減る

  return 0;
}

以下は実行結果。

count = 1 Addr = 0x556ed863f2b0
count = 2 Addr = 0x556ed863f2b0
count = 3 Addr = 0x556ed863f2b0
count = 1 Addr = 0x556ed863f700
count = 2 Addr = 0x556ed863f2b0  ← resetしたので、参照回数が減っている

operator boolでshaed_ptrが有効か否かのチェック、swapメソッドでポインタの交換が出来るのもunique_ptrと同様。

shared_ptrのちょっとした問題

shared_ptrは参照数で管理しているので、データ構造によっては直接的、間接的にshared_ptr同士が参照し合う現象(相互参照)が発生してメモリ領域が開放されない場合がある。以下の例は、多数の線分の接続関係からポリゴンを作成するロジックから抜粋したもの。

#include <iostream>
#include <memory>

// 直線クラス
class CPrimitiveSegment {
 public:
  CPrimitiveSegment() : sx(0), sy(0), ex(0), ey(0) {
    std::cout << "CPrimitiveSegment::CPrimitiveSegment()" << std::endl;
  }
  ~CPrimitiveSegment() {
    std::cout << "CPrimitiveSegment::~CPrimitiveSegment()" << std::endl;
  }

  std::shared_ptr<CPrimitiveSegment> next;  // 接続先
  int sx, sy, ex, ey;
};

int main(int argc, char* argv[]) {
  // 線分を3本定義
  std::shared_ptr<CPrimitiveSegment> s1{std::make_shared<CPrimitiveSegment>()};
  std::shared_ptr<CPrimitiveSegment> s2{std::make_shared<CPrimitiveSegment>()};
  std::shared_ptr<CPrimitiveSegment> s3{std::make_shared<CPrimitiveSegment>()};

  // 三角形を作る
  s1->next = s2;
  s2->next = s3;
  s3->next = s1;

  // shared_ptrの参照数を出力
  std::cout << "count s1 = " << s1.use_count() << std::endl;
  std::cout << "count s2 = " << s2.use_count() << std::endl;
  std::cout << "count s3 = " << s3.use_count() << std::endl;

  return 0;
}

実行結果

CPrimitiveSegment::CPrimitiveSegment()
CPrimitiveSegment::CPrimitiveSegment()
CPrimitiveSegment::CPrimitiveSegment()
count s1 = 2
count s2 = 2
count s3 = 2

参照回数が2のまま、main関数が終了しデストラクタが実行されない。いわゆる「メモリリーク」が発生する。
この場合、参照関係を一方通行にして、参照される側の参照カウンタを「変化させない」weak_ptrを使用する。

#include <iostream>
#include <memory>

class CPrimitiveSegment {
 public:
  CPrimitiveSegment() : sx(0), sy(0), ex(0), ey(0) {
    std::cout << "CPrimitiveSegment::CPrimitiveSegment()" << std::endl;
  }
  ~CPrimitiveSegment() {
    std::cout << "CPrimitiveSegment::~CPrimitiveSegment()" << std::endl;
  }
  // shared_ptrから参照数を変化させないweak_ptrへ
  std::weak_ptr<CPrimitiveSegment> next;  // 接続先
  int sx, sy, ex, ey;
};

int main(int argc, char* argv[]) {
  // 線分を3本定義
  std::shared_ptr<CPrimitiveSegment> s1{std::make_shared<CPrimitiveSegment>()};
  std::shared_ptr<CPrimitiveSegment> s2{std::make_shared<CPrimitiveSegment>()};
  std::shared_ptr<CPrimitiveSegment> s3{std::make_shared<CPrimitiveSegment>()};

  // 三角形を作る
  s1->next = s2;
  s2->next = s3;
  s3->next = s1;

  // shared_ptrの参照数を出力
  std::cout << "count s1 = " << s1.use_count() << std::endl;
  std::cout << "count s2 = " << s2.use_count() << std::endl;
  std::cout << "count s3 = " << s3.use_count() << std::endl;

  return 0;
}

以下は実行結果。参照回数は変化せずにデストラクタが正しく実行されている。

CPrimitiveSegment::CPrimitiveSegment()
CPrimitiveSegment::CPrimitiveSegment()
CPrimitiveSegment::CPrimitiveSegment()
count s1 = 1
count s2 = 1
count s3 = 1
CPrimitiveSegment::~CPrimitiveSegment()
CPrimitiveSegment::~CPrimitiveSegment()
CPrimitiveSegment::~CPrimitiveSegment()

shared_ptrをmake_uniqueで初期化?

なぜ、そうなのかは判らないのだけど、shared_ptrはunique_ptrを生成するstd::make_uniqueで初期化出来てしまう。
以下のコードは、一見おかしいのだけどコンパイル出来て動いてしまう。

#include <iostream>
#include <memory>

class CPrimitiveSegment {
 public:
  CPrimitiveSegment() : sx(0), sy(0), ex(0), ey(0) {
    std::cout << "CPrimitiveSegment::CPrimitiveSegment()" << std::endl;
  }
  ~CPrimitiveSegment() {
    std::cout << "CPrimitiveSegment::~CPrimitiveSegment()" << std::endl;
  }
  std::weak_ptr<CPrimitiveSegment> next;  // 接続先
  int sx, sy, ex, ey;
};

int main(int argc, char* argv[]) {
  std::shared_ptr<CPrimitiveSegment> s1{std::make_unique<
      CPrimitiveSegment>()};  // shared_ptrをmake_uniqueで初期化している。
  return 0;
}

個人的な見解だけど、unique_ptrとshared_ptrはポインタとしての性質が違うからエラーになってもいいような気がする。

スマートポインタの使い方 その1:unique_ptr

スマートポインタって何?

C++において、operator newでメモリ領域(ヒープ領域)を動的に確保した場合、その領域はoperator deleteでプログラマが責任を持って解放してやらなければならない。しかし、deleteを書き忘れたり、例外が発生したときの処理を怠った場合など、それが正しく行われないことはよくある。正しく解放されなかった領域はOSやプロセスが使用可能なメモリ領域を「不正占拠」し、それが積もり積もると、OSやプロセスが停止する場合がある。

#include <memory>
class Mess {};
void f(Point p1, Point p2)
{
  Rectangle* r(new Rectangle(p1, p2));
  r->rotate(45);  // 矩形を45度回転
  // ...
  if (in_a_mess) throw Mess(); // 例外を投げてみる

  // ねぇ、例外が投げられたときの後始末は?
  delete r;
}

昔話だけど、C++11より前の「古い」C++はC由来であるためにメモリなどのリソースの管理はプログラマに委ねられていた。例えば、Cで可変長配列を実装するとなると本当に面倒。
そこで、C++11でプログラマの負担を軽減すべく、メモリ領域の「寿命」の管理を自動的に行う「スマートポインタ」が実装された。
スマートポインタとは文字通り"smart"(気の利いた)なポインタのことで、スマートポインタが管理しているメモリ領域が不要になったら自動的に解放してくれる。*1メモリ領域を解放するタイミングの違いにより2種類のタイプがある。

  • std::unique_ptr : 変数の寿命が尽きるとそのメモリ領域を自動的に開放する。
  • std::shared_ptr : 参照カウントを持ち、参照カウントがゼロになるとそのメモリ領域を開放する。

もちろん、振る舞いはあくまでも「ポインタ」なので、見た目は通常のポインタ関連のコードと何ら変わりが無い。

#include <memory>
class Mess {};
void f(Point p1, Point p2)
{
  std::unique_ptr<Rectangle> r(new Rectangle(p1, p2)); // unique_ptrはブロックの外に達するとメモリ領域を自動開放する
  r->rotate(45);  // 矩形を45度回転
  
  // ...
  if (in_a_mess) throw Mess(); // 例外を投げてみる
  // 途中で例外が投げられてもブロックの外に出たら自動的にメモリ領域の開放を行う
  // operator deleteは不要。スマートポインタが解放する。
}

std::unique_ptrを使ってみる

std::unique_ptrはブロックの外に抜けるなど変数の寿命が尽きたら、管理しているメモリ領域を自動的に開放するスマートポインタ。その名の通り"unique"(唯一)のメモリ領域しか管理しないので、複数のunique_ptrのインスタンスが同一のメモリ領域を管理することはあり得ない。かつてC++の標準ライブラリにはスマートポインタとしてauto_ptrがあったのだけど、仕様に少々問題があった為にC++11からは使用が非推奨となり、C++17で削除された。その替わりとしてunique_ptrが導入された。
使い方はstd:make_unique関数を使ってオブジェクトを生成するだけ。*2以後、そのオブジェクトが占有するメモリ領域はunique_ptrの管理下に置かれる。使用するシチュエーションとして考えられるのは、pimplイディオムの実装部分や、Abstract Factoryパターンが生成したオブジェクトを自動的に解放したい場合、あとは、シングルトンオブジェクトとか。

#include <iostream>
#include <memory>
class TestObject
{
public:
    TestObject()
    {
        std::cout << "TestObject::Constructor" << std::endl;
    }
    ~TestObject()
    {
        std::cout << "TestObject::Destructor" << std::endl;
    }
    void message()
    {
        std::cout << "TestObject::message" << std::endl;
    }
};

int main()
{
    std::unique_ptr<TestObject> pObject{std::make_unique<TestObject>()} // オブジェクトをコンストラクタに渡す
    pObject->message(); // メンバ関数の呼び出し
    return 0;
}

メンバへのアクセスはポインタと同様にアロー演算子(operator->)で行う。インスタンスへのアクセスはポインタ演算子(operator*)。当然、deleteも必要ない。ブロックの外に出ると変数の寿命が尽きるのでunique_ptrはデストラクタで自身が管理しているメモリ領域を開放する。ただし、operator=で代入することは出来ない。

void f(Point p1, Point p2)
{
  std::unique_ptr<Rectangle> rb{std::make_unique<>(p1, p2)}; // 初期化はmake_uniqueで
}

配列として連続した複数個のメモリ領域が欲しい場合は、[]をつけて宣言する。

#include <iostream>
#include <memory>
int main() {
  constexpr int kArraySize{10};
  std::unique_ptr<int[]> a{std::make_unique<int[]>(kArraySize)};  // 10要素分生成

  for (int i = 0; i < kArraySize; ++i) {
    a[i] = i;
  }

  for (int i = 0; i < kArraySize; ++i) {
    std::cout << a[i] << std::endl;
  }
  return 0;
}

生ポインタの取得

unique_ptrはあくまでもポインタをラップするユーティリティクラス。コードの見た目をポインタのように見せているだけであり、型そのものが違う。Windows APIUnix/Linuxシステムコールのような標準C++ではないAPIやCのライブラリを呼び出す場合、unique_ptrが管理しているメモリ領域の生ポインタが必要になる場合がある。unique_ptrが管理しているメモリ領域の生ポインタを取得するにはgetメソッドを使用する。注意しなければならないのはgetメソッドはメンバ演算子(operator.)で呼び出す。

// 地図の名前を取得するAPI
int get_map_name(int MapID, char* buffer);
void process_map_name(int MapID)
{
  int length = get_map_name(MapID, NULL);  // 必要な領域サイズを取得
  std::unique_ptr<char[]> buffer{std::make_unique<char[]>(length)};  // 領域を確保

  get_map_name(MapID, buffer); // エラー!!!
  get_map_name(MapID, buffer.get()); // getメソッドで生ポインタを取得する
}

取得した生ポインタはあくまでもunique_ptrの管理下にあるので、自分でdeleteしてはならない。unique_ptrがメモリ領域を管理しているか否かは、getメソッドで生ポインタを取得するか、あるいは、operator boolでチェックする。

void f() 
{
  std::unique_ptr<TestObject> obj;  // どこも管理していないunique_ptr
  if (!obj) {
    std::cout << "ptr is not enabled" << std::endl;
  }
}

unique_ptrの再初期化

Abstract Factoryパターンのようにサブクラスで振る舞いが変化するオブジェクトを動的に生成する場合など、文脈によってunique_ptrを再初期化する必要が出てくる。unique_ptrはoperator=で代入(上書き)出来ないが、resetメソッドで別のポインタで再初期化できる。このとき、unique_ptrは自身が管理しているメモリ領域を解放するので、プログラマがdeleteでメモリ領域を明示的に開放する必要は無い。

#include <iostream>
#include <memory>

class TestObject {
 public:
  TestObject(int ID) : m_ID(ID) {
    std::cout << "TestObject::Constructor" << "ID = " << m_ID << std::endl;
  }
  ~TestObject() {
    std::cout << "TestObject::Destructor" << "ID = " << m_ID << std::endl;
  }
  void message() { std::cout << "TestObject::message" << std::endl; }
  int m_ID;
};

int main() {
  std::unique_ptr<TestObject> pObject{new TestObject(100)};
  pObject->message();  // メンバ関数の呼び出し

  // 再初期化
  pObject.reset(new TestObject(200));
  return 0;
}

上記コードの実行結果

TestObject::ConstructorID = 100
TestObject::message
TestObject::ConstructorID = 200
TestObject::DestructorID = 100
TestObject::DestructorID = 200

resetの引数にnullptrを渡せば、明示的にunique_ptrが管理しているメモリ領域を解放することが出来る。メモリ領域を開放せずに、ただ単にunique_ptrとメモリ領域の関係を切り離す場合はreleaseメソッドを使用する。戻り値としてメモリ領域の生ポインタが返るので、その後は通常のポインタとして扱える。当然、解放が必要であれば明示的に解放しなければならない。

void f()
{
  std::unique_ptr<TestObject> pObject{std::make_unique<TestObject>()};
  TestObject* pp = pObject.release(); // unique_ptrとメモリ領域の関係を切り離す

  delete pp; // 明示的に解放しないとリソースリークする
}

unique_ptrと代入演算子(operator=)

C++03まで標準であったauto_ptrが「問題」とされたのは、auto_ptr同士をoperator=で代入するとメモリ領域の所有者が=の右から左へ移り、代入元のauto_ptrの管理領域はNULLとなる為である。

#include <iostream>
#include <memory>
int main()
{
  std::auto_ptr<int> a(new int(100));
  std::auto_ptr<int> b(new int(200));

  // 単なるポインタ同士の代入のつもりだが、実はポインタの所有権が移動する
  b = a;
  
  // aはどこも参照していない抜け殻なので、Access Violationが発生
  std::cout << *a << std::endl;

  return 0;
}

一見問題なさそうなコードだけど、単純な代入文であるにもかかわらず副作用が発生するのはよろしくないので、unique_ptrはoperator=を隠蔽して明示的な代入を出来なくした。

#include <iostream>
#include <memory>
int main()
{
  std::unique_ptr<int> a{std::make_unique<int>(100)};
  std::unique_ptr<int> b;

  b = a; // operator=は隠蔽されているのでコンパイルエラーが発生する。
  std::cout << *a << std::endl;

  return 0;
}

unique_ptrをメンバに持つクラスにoperator=を定義する場合など、文脈によってはunique_ptr同士で「値」をやりとりしなければならない場合がある。unique_ptrは単一の領域しか管理しないので、単一性を担保するための戦略として「中身」のやりとり以外に、管理しているメモリ領域のポインタ値の交換、もしくは譲渡が考えられる。

#include <iostream>
#include <memory>
int main() {
  std::unique_ptr<int> a{std::make_unique<int>(100)};
  std::unique_ptr<int> b{std::make_unique<int>(200)};

  std::cout << "a = " << *a << std::endl;
  std::cout << "b = " << *b << std::endl;

  *a = *b;  // unique_ptrの管理下にある領域の「中身」をやりとりする

  std::cout << "a = " << *a << std::endl;
  std::cout << "b = " << *b << std::endl;

  return 0;
}

unique_ptrが管理している領域のポインタ値を「交換」する場合はswapメソッドを使う。

#include <iostream>
#include <memory>
int main() {
  std::unique_ptr<int> a{std::make_unique<int>(100)};
  std::unique_ptr<int> b{std::make_unique<int>(200)};

  std::cout << "a = " << *a << std::endl;
  std::cout << "b = " << *b << std::endl;
  a.swap(b);  // unique_ptrが管理している領域を交換
  std::cout << "a = " << *a << std::endl;
  std::cout << "b = " << *b << std::endl;

  return 0;
}

ポインタを「譲渡」する場合は、C++11で導入されたmove関数を使用する。

#include <iostream>
#include <memory>
int main() {
  std::unique_ptr<int> a{std::make_unique<int>(100)};
  std::unique_ptr<int> b{};

  if (a) std::cout << "a = " << *a << std::endl;
  if (b) std::cout << "b = " << *b << std::endl;
  b = std::move(a);  // unique_ptrが管理しているポインタを譲渡
  if (a) std::cout << "a = " << *a << std::endl;
  if (b) std::cout << "b = " << *b << std::endl;

  return 0;
}

move関数はオブジェクトが保持しているリソースを戻り値に譲渡*3させるヘルパ関数で、結果、譲渡元には何も残らない。C++11で導入された「ムーブセマンティクス」と「rvalue参照」を用いて実装されている。

*1:厳密にはヒープ領域だけでなく、ハンドルといったメモリとは少々違う意味合いのリソースも管理できるが、これは別項で。

*2:newでオブジェクトを生成しても良いのだけど静的解析で指摘される場合がある。

*3:厳密には、lvalueをrvalueに変換

 CHUWI HeroBoxにLinuxディストリビューションをインストール

前口上

小型で気軽に使えるLinuxサーバが欲しくてCHUWI HeroBoxをゲット。 試行錯誤をしまくって、何とかモノになったので覚え書き。

スペックとかレビューは色々な人が紹介しているので省略。 このPCは元々256GBのSSDを内蔵しているけど、手元に512GBの2.5インチSSDが余っていたのでこれを増設。

Linuxディストリビューションの選択

結局、Ubuntu Server 20.10に落ち着いた。 他のインストール候補はRed Hat Enterprise Linux 8とCentOS 8。これら以外にFedoraとかCentOS 8 Streamを入れたけどすぐに消した。一時、RHEL8で使っていたのだけど、Docker絡みとPostgreSQL絡みでRHELは諦めた。

HeroBoxへのLinuxのインストール

起動とインストール

LinuxのインストールはインストールイメージをダウンロードしてRufusUSBメモリに書込み。 USBメモリを前面右側のUSB3.0ポートに刺して起動。F7を連打すればブートメディアを選択できる。RHELの場合はブート可能なパーティションが2つあるので第2パーティションからブート。オフラインインストールとなる。オフラインインストール後にアクティベーションを行えばパッケージの更新が出来る。 第1パーティションはネットワークインストール用。最初はこれを選択したもののインストール中にRHELアクティベーションが上手く行かなかったので挫折しかかった。

Ubuntu Server 20.10の場合はインストーラーの指示に従えばOK。 インストール先は512GBのSSDを選択。LVMでフォーマットするのでインストール後に256GB側を拡張して1つのファイルシステムにする。 RHELはインストール時に2台のSSDを1つの論理パーティションにしてインストールできた。

インストール後のディスクの構成はこんな感じ。

# df -hP
Filesystem                         Size  Used Avail Use% Mounted on
tmpfs                              778M  1.3M  777M   1% /run
/dev/mapper/ubuntu--vg-ubuntu--lv  456G  6.3G  427G   2% /
tmpfs                              3.8G     0  3.8G   0% /dev/shm
tmpfs                              5.0M     0  5.0M   0% /run/lock
tmpfs                              4.0M     0  4.0M   0% /sys/fs/cgroup
/dev/sda2                          976M  112M  798M  13% /boot
/dev/sda1                          511M  7.9M  504M   2% /boot/efi
tmpfs                              778M  4.0K  778M   1% /run/user/1000

既存のファイルシステムに空きドライブを追加して1つのファイルシステムにする

物理ディスクが複数ある場合、これを1つに統合するのは信頼性が低下するので宜しくないけど、運用の柔軟性をとってRAID0構成に。 以下の手順で既存の/dev/sdaに/dev/sdbを追加して1つのファイルシステムにした。

  1. パーティション作成
  2. 物理ボリューム作成
  3. 既存のボリュームグループに作成した物理ボリュームを追加
  4. ボリュームグループの拡張
  5. ファイルシステムの拡張

パーティション作成

パーティションを作成する前にvgdisplayで既存のボリュームグループの状態を確認する。

# vgdisplay
  --- Volume group ---
  VG Name               ubuntu-vg
  System ID
  Format                lvm2
  Metadata Areas        1
  Metadata Sequence No  2
  VG Access             read/write
  VG Status             resizable
  MAX LV                0
  Cur LV                1
  Open LV               1
  Max PV                0
  Cur PV                1
  Act PV                1
  VG Size               <464.26 GiB
  PE Size               4.00 MiB
  Total PE              118850
  Alloc PE / Size       118850 / <464.26 GiB
  Free  PE / Size       0 / 0
  VG UUID               eljjjs-eqqz-wes6-fkSC-SQUQ-P8wI-P4KETF

ボリュームグループはインストール時に選択した512GBのSSD(/dev/sda)に作成されている。これに、空である256GB側(/dev/sdb)を追加する。 まずは、gdiskで/dev/sdbでパーティションを編集

# gdisk /dev/sdb
GPT fdisk (gdisk) version 1.0.5

pコマンドでパーティションの状態を確認。

Command (? for help): p
Disk /dev/sdb: 500118192 sectors, 238.5 GiB
Model: KINGSTON RBUSNS8
Sector size (logical/physical): 512/512 bytes
Disk identifier (GUID): 5394AF49-2144-4AB8-9D79-BBFB489DCCBC
Partition table holds up to 128 entries
Main partition table begins at sector 2 and ends at sector 33
First usable sector is 34, last usable sector is 500118158
Partitions will be aligned on 2048-sector boundaries
Total free space is 2014 sectors (1007.0 KiB)

Number  Start (sector)    End (sector)  Size       Code  Name
  1            2048       500118158   238.5 GiB   8300  Linux filesystem

dコマンドでパーティションを削除。

Command (? for help): d
Using 1

再度、pコマンドでパーティションが削除されていることを確認。

Command (? for help): p
Disk /dev/sdb: 500118192 sectors, 238.5 GiB
Model: KINGSTON RBUSNS8
Sector size (logical/physical): 512/512 bytes
Disk identifier (GUID): 5394AF49-2144-4AB8-9D79-BBFB489DCCBC
Partition table holds up to 128 entries
Main partition table begins at sector 2 and ends at sector 33
First usable sector is 34, last usable sector is 500118158
Partitions will be aligned on 2048-sector boundaries
Total free space is 500118125 sectors (238.5 GiB)

Number  Start (sector)    End (sector)  Size       Code  Name

nコマンドでLVMパーティションを作成。GUIDコードは8E00を入力。LVMパーティションとなる。

Command (? for help): n
Partition number (1-128, default 1):
First sector (34-500118158, default = 2048) or {+-}size{KMGTP}:
Last sector (2048-500118158, default = 500118158) or {+-}size{KMGTP}:
Current type is 8300 (Linux filesystem)
Hex code or GUID (L to show codes, Enter = 8300): 8E00
Changed type of partition to 'Linux LVM'

作成したパーティションを確認。

Command (? for help): p
Disk /dev/sdb: 500118192 sectors, 238.5 GiB
Model: KINGSTON RBUSNS8
Sector size (logical/physical): 512/512 bytes
Disk identifier (GUID): 5394AF49-2144-4AB8-9D79-BBFB489DCCBC
Partition table holds up to 128 entries
Main partition table begins at sector 2 and ends at sector 33
First usable sector is 34, last usable sector is 500118158
Partitions will be aligned on 2048-sector boundaries
Total free space is 2014 sectors (1007.0 KiB)

Number  Start (sector)    End (sector)  Size       Code  Name
  1            2048       500118158   238.5 GiB   8E00  Linux LVM

wコマンドでパーティション情報をディスクに書込む。

Command (? for help): w

    Final checks complete. About to write GPT data. THIS WILL OVERWRITE EXISTING
    PARTITIONS!!

    Do you want to proceed? (Y/N): y
    OK; writing new GUID partition table (GPT) to /dev/sdb.
    The operation has completed successfully.

物理ボリューム作成

pvcreateコマンドで/dev/sdbに物理ボリュームを作成。

# pvcreate /dev/sdb1
  Physical volume "/dev/sdb1" successfully created.

物理ボリュームのボリュームグループへの追加

vgextendコマンドで作成した物理ボリュームを既存のボリュームグループに追加

# vgextend ubuntu-vg /dev/sdb1
  Volume group "ubuntu-vg" successfully extended

vgdisplayでボリュームグループが拡張可能になったことを確認する

# vgdisplay
  --- Volume group ---
  VG Name               ubuntu-vg
  System ID
  Format                lvm2
  Metadata Areas        2
  Metadata Sequence No  3
  VG Access             read/write
  VG Status             resizable
  MAX LV                0
  Cur LV                1
  Open LV               1
  Max PV                0
  Cur PV                2
  Act PV                2
  VG Size               702.73 GiB
  PE Size               4.00 MiB
  Total PE              179899
  Alloc PE / Size       118850 / <464.26 GiB
  Free  PE / Size       61049 / 238.47 GiB
  VG UUID               eljjjs-eqqz-wes6-fkSC-SQUQ-P8wI-P4KETF

ボリュームグループの拡張

lvextendでボリュームグループを拡張する。+100%FREEで追加した全領域全てを拡張する。

# lvextend -l +100%FREE /dev/ubuntu-vg/ubuntu-lv
  Size of logical volume ubuntu-vg/ubuntu-lv changed from <464.26 GiB (118850 extents) to 702.73 GiB (179899 extents).
  Logical volume ubuntu-vg/ubuntu-lv successfully resized.

vgdisplayで論理ボリュームが拡張されたことを確認。

# vgdisplay
--- Volume group ---
VG Name               ubuntu-vg
System ID
Format                lvm2
Metadata Areas        2
Metadata Sequence No  4
VG Access             read/write
VG Status             resizable
MAX LV                0
Cur LV                1
Open LV               1
Max PV                0
Cur PV                2
Act PV                2
VG Size               702.73 GiB
PE Size               4.00 MiB
Total PE              179899
Alloc PE / Size       179899 / 702.73 GiB
Free  PE / Size       0 / 0
VG UUID               eljjjs-eqqz-wes6-fkSC-SQUQ-P8wI-P4KETF

ファイルシステムの拡張

dfで確認したところ、ボリュームが拡張されてもファイルシステムは拡張されていない。

# df -kh
Filesystem                         Size  Used Avail Use% Mounted on
tmpfs                              778M  1.4M  777M   1% /run
/dev/mapper/ubuntu--vg-ubuntu--lv  456G  6.3G  427G   2% /
tmpfs                              3.8G     0  3.8G   0% /dev/shm
tmpfs                              5.0M     0  5.0M   0% /run/lock
tmpfs                              4.0M     0  4.0M   0% /sys/fs/cgroup
/dev/sda2                          976M  112M  798M  13% /boot
/dev/sda1                          511M  7.9M  504M   2% /boot/efi
tmpfs                              778M  4.0K  778M   1% /run/user/1000

resize2fsで拡張したボリュームを拡張する。

# resize2fs /dev/ubuntu-vg/ubuntu-lv
resize2fs 1.45.6 (20-Mar-2020)
Filesystem at /dev/ubuntu-vg/ubuntu-lv is mounted on /; on-line resizing required
old_desc_blocks = 59, new_desc_blocks = 88
The filesystem on /dev/ubuntu-vg/ubuntu-lv is now 184216576 (4k) blocks long.

dfで確認すると、ファイルシステムが拡張されていることが確認できた。

# df -kh
Filesystem                         Size  Used Avail Use% Mounted on
tmpfs                              778M  1.4M  777M   1% /run
/dev/mapper/ubuntu--vg-ubuntu--lv  691G  6.3G  652G   1% /
tmpfs                              3.8G     0  3.8G   0% /dev/shm
tmpfs                              5.0M     0  5.0M   0% /run/lock
tmpfs                              4.0M     0  4.0M   0% /sys/fs/cgroup
/dev/sda2                          976M  112M  798M  13% /boot
/dev/sda1                          511M  7.9M  504M   2% /boot/efi
tmpfs                              778M  4.0K  778M   1% /run/user/1000

これで、ファイルシステムの拡張は完了。

日本語化

タイムゾーンの変更

タイムゾーンUTCになっているので、timedatectlコマンドでタイムゾーンJSTに変更する。

# date
Mon Feb 22 15:02:44 UTC 2021
# timedatectl set-timezone Asia/Tokyo
# date
Tue Feb 23 00:02:58 JST 2021

ロケールの変更

「Shift-JIS/EUC-JP死すべし慈悲はない」なので、ロケールはja_JP.UTF-8に。 日本語マニュアルも入れる。

# localectl set-locale LANG=ja_JP.UTF-8
# apt install -y language-pack-ja manpages-ja manpages-ja-dev

sambaのインストール

普通にapt install sambaでインストール。pdbeditでユーザー追加

# sudo apt install samba
# pdbedit -a [ユーザー名]

smb.confはglobalセクションにWORKGROP名とNetBios名を追加。homesセクションでホームディレクトリをWindows側から見えるように。

    [global]
        workgroup = WORKGROUP
        netbios name = [ホスト名]

    [homes]
        comment = Home Directories
        browseable = no
        read only = no

鍵ペアによるsshログイン

インストール時にSSHサーバーを有効にしたので、鍵ペアをつかってWindowからログイン出来るようにする。

% ssh-keygen
% cd .ssh
% mv id_rsa.pub authorized_keys

出来たid_rsaをホストのWindowsのユーザーフォルダの下の.sshフォルダにコピー。 コマンドプロンプトからssh [ユーザー名]@[ホスト名]でログイン出来る。 Windows Terminalの設定はこんな感じ。

{
  (略)
  "profiles":
  {
      "list":
      [
            (略)
          {
              "name": "[ホスト名](ssh)",
              "commandline": "ssh [ユーザー名]@[ホスト名]",
              "hidden": false,
              "icon": "(アイコンのパス)",
              "colorScheme": "Sakura"
          }
      ]
  }
}

アイコンはUbuntuのアイコンをググって32x32にしてOneDriveに配置したのを参照。 カラースキームはWindows Terminal Themesから。カラースキームを変えることでログイン先を区別出来るようにした。

実験:空のstd::vectorから要素を取得するとどうなるか?

ふとした疑問

std::vectorには最後尾の要素を取得するbackというメソッドがあります。 もし、vectorが空の場合backメソッドを呼ぶとどうなるのか実験してみました。

書いたコードはこんな感じ。期待する動作は空要素を返すか、何らかの例外を投げてくれるか。

#include <vector>
#include <string>
#include <iostream>
int main(int argc, char* argv[])
{
    std::vector<std::string> StrList;

    try {
            auto v = StrList.back();
        std::cout  << "Length = " << v.length() << std::endl;
    } catch (std::exception& e) {
       std::cout  << "exception:" << e.what()  << std::endl;
    }

    return 0;
}

実験結果

試したのはbcc32c 7.40、MS-C 19.16.27026.1 、gcc 7.3.0、clang 6.0.0。gccとclangはWSLで確認。 結果は以下の通り。

コンパイラ 結果
bcc32c 例外やエラーなし。
MS-C 例外やエラーなし。
gcc segmentation fault
clang segmentation fault

Windows処理系だと、例外すら発生せずにどこかでコケている感じ。 bcc32cはデバッガで確認したり、Dinkumware STLのソースを確認すると、単に「end() - 1」だったので、そりゃあコケるわなと。

まとめのようなモノ

ということで、コンテナから要素を取り出す場合emptyとかでチェックしないと駄目というオチでした。

FireDAC / C++Builder のちょっとした小ネタ×3

その1:結果が1行、1列しかないデータを取得する場合

データベース開発をしていてよくあるのがクエリを発行して1個のスカラー値しか取らないケース。
例えば、連番とか、翌営業日とか、件数などなど。
こんな場合、TFDQuery::OpenメソッドでSQLを発行して、データセットから値を取得して、クローズするのはあまり「美しくない」です。
そんな場合に打って付けなのが、TFDConnection::ExecSQLScalarメソッドです。
このメソッドはTFDQuery::Openメソッドと同じようにクエリを発行するのですが、単一のスカラー値をVariantで返却します。データセットの操作とかは不要です。
文字通り、単一のスカラー値を取得する場合に使ってみて下さい。

その2:データ型マッピングと日付型

FireDACのデータ型マッピングを皆様お使いでしょうか?
この機能はRDBMSとクライアント間のデータ変換ルールを定義します。ドキュメントでは数値の変換ルールが記載されていますが、日時型はどうでしょうか?
FireDACというか、Delphi / C++Builderのデータセットで使われる日時型は以下の2つです。

さて、この両者の違いは秒未満の精度の違いです。RDBMSの日時型がどちらにマッピングされるのは、RDBMSに依存します。
FireDACのウリはマルチデータベース対応ですが、ここで依存性が発生するのはちょっとよろしくないです。
そこで、FireDAC接続エディタを使い明示的にマッピングすることで、日時型をどちらかに寄せることが出来ます。これで、TFDDConnectionで違うRDBMSに接続しても型変換を担保することが出来ます。

f:id:A7M:20181218225520p:plain
FireDAC接続エディタ

ちなみに、SQL ServerのDateTime2型は何もしないと文字列型にマッピングされます。
これは、データベースドライバの関係なので、SQL Native Client をインストールすることを強くお勧めします。(Microsoft SQL Server への接続(FireDAC) - RAD Studio

その3:FireMonkeyとC++文字列リテラル

C++Builderで作ったライブラリをFireMonkeyのプロジェクトにポーティングをすることは良くあるかと思います。
プラットフォーム毎に文字列リテラル文字コードを意識しないと文字化けします。
# ええ、テストプログラムで見事にやらかしました。
最近だと、Unicodeを意識して、文字列の接頭辞にuとかu8とかLとか付けますが、_D()マクロを使うとプラットフォームに合わせた接頭辞を選択してくれるので大変便利です。
詳細は、以下のDocWikiの記事を参考にしてください。
C++ におけるマルチデバイス アプリケーションについての考慮事項 - RAD Studio

Delphiでラムダ式

Delphi 10.3 Rioでは、Delphi言語のエンハンスとして、インライン変数宣言インライン変数の型推論が導入されました。

これによりDelphiラムダ式(無名メソッド)が書きやすくなりましたのでご紹介します。

ソースコードラムダ式を使うメリットとしては以下が挙げられます。

  • スレッドやコールバックのようなロジックをインラインで書くことによって可読性が向上する。
  • 判定条件などの処理をオブジェクトとして持つことで記載が簡潔になる。

10.2まで、Delphiで無名メソッドを使用するにはTypeブロック内で以下のように宣言する必要があります。

program LTDel;
{$APPTYPE CONSOLE}
{$R *.res}
uses
  System.SysUtils;
type
  TMyFunc = reference to procedure();
var
  f0 : TMyFunc;
  f1 : TMyFunc;
begin
  f0 :=
    procedure begin
      WriteLn('Hello,')
    end;
  f1 :=
    procedure begin
      WriteLn('World!')
    end;
  f0();
  f1();
end.

そして、10.3 Rioからは、以下のように記述出来ます。ラムダ式を配列として宣言した例です。型推論のおかげでかなりラムダ式しています。

program LTDel;
{$APPTYPE CONSOLE}
{$R *.res}
uses
  System.SysUtils;

begin
  var f := [
    procedure begin
      WriteLn('Hello,')
    end,
    procedure begin
      WriteLn('World!')
    end
  ];

  f[0]();
  f[1]();
end.

一見、ラムダ式は難しく見えますが、理解して使い始めた途端に可読性が良くなりますので、是非とも使ってみて下さい。