operator=()をoverrideできるのか?

operator=()をoverrideできるのか?
できるかできないかであれば、できる。
しかし、しない方が良いだろう。

ベースクラスのポインターを保持する関数などで派生クラスを含めたコピーをしたい場合どうなるか。
確認をした。
なお今回のお題では呼び出し側でのキャストはなしにする(複数の派生クラスを使っている場合を想定)。

// ベースクラス
class TBase
{
  protected:
    int n;
  public:
    TBase( int s ) : n(s) {}
    TBase(            const TBase&  ) = default;
    TBase(                  TBase&& ) = default;
    TBase& operator=( const TBase&  ) = default;
    TBase& operator=(       TBase&& ) = default;
    virtual ~TBase()                  = default;

            int get_n() { return n; }
    virtual int get_m() = 0;
};
//---------------------------------------------------------------------------
// 派生クラス
class TChild1 : public TBase
{
  protected:
    int m;
  public:
    TChild1( int s, int p ) : TBase(s), m(p) {}
    TChild1(            const TChild1&  ) = default;
    TChild1(                  TChild1&& ) = default;
    TChild1& operator=( const TChild1&  ) = default;
    TChild1& operator=(       TChild1&& ) = default;
    virtual ~TChild1()                    = default;

    virtual int get_m() override { return m; }
};

上のような環境で、ベースクラスのポインターでコピーをするとどうなるか。

  TBase* s1 = new TChild1(2, 12);
  TBase* s2 = new TChild1(3, 13) ;

  *s2 =  *s1; // s2->n=2, s2->m=13

operator=()の呼び出しは、TBaseのoperator=()が用いられる。TChild1のoperator=()は引数が違うので用いられない。
TBaseとTChildのoperator=()にvirtualをつけても、引数が違うので別の仮想関数になり、やはりTChild1のoperator=()は呼び出しされない。
もちろん下のように両方のポインターをキャストすればできるが、その場合はvirtualを付けなくても変わらない。
但し、キャストできたか確認が必要になる。

  TBase* s1 = new TChild1(2, 12);
  TBase* s2 = new TChild1(3, 13) ;

  *dynamic_cast<TChild1*>(s2) =  *dynamic_cast<TChild1*>(s1); // s2->n=2, s2->m=12

では、TBaseのoperator=()をvitrualにして、派生関数側でも定義すればどうか?

class TBase
{
  protected:
    int n;
  public:
    TBase( int s ) : n(s) {}
    TBase(            const TBase&  ) = default;
    TBase(                  TBase&& ) = default;
    virtual TBase& operator=( const TBase&  ) = default;
    virtual TBase& operator=(       TBase&& ) = default;
    virtual ~TBase()                  = default;

            int get_n() { return n; }
    virtual int get_m() = 0;
};
//---------------------------------------------------------------------------
class TChild2 : public TBase
{
  protected:
    int m;
  public:
    TChild2( int s, int p ) : TBase(s), m(p) {}
    TChild2(            const TChild2&  ) = default;
    TChild2(                  TChild2&& ) = default;
    TChild2& operator=( const TChild2&  ) = default;
    TChild2& operator=(       TChild2&& ) = default;
    virtual ~TChild2()                    = default;

    virtual TBase& operator=( const TBase&  lhs ) override { const TChild2&  Lhs = static_cast<const TChild2& >(lhs); return operator=( Lhs ); };

    virtual int get_m() override { return m; }
};

おなじく、ベースクラスのポインターでコピーをするとどうなるか。

  TBase* s1 = new TChild2(2, 12);
  TBase* s2 = new TChild2(3, 13) ;

  *s2 =  *s1; // s2->n=2, s2->m=12

operator=()の呼び出しは、TChild2のoperator=( const TBase& )が用いられる。
この書き方で、意図した結果を得られることが判った。
ただし、この書き方をすると保守管理が大変なことが予想できる。=の挙動が場合によって変わる可能性もあるからだ。
例えば、s1が別の派生クラスであった場合、結果は悲惨なことになる。

やはり別関数を導入した方が良いのだろう。

class TBase
{
  protected:
    int n;
  public:
    TBase( int s ) : n(s) {}
    TBase(            const TBase&  ) = default;
    TBase(                  TBase&& ) = default;
    TBase& operator=( const TBase&  ) = default;
    TBase& operator=(       TBase&& ) = default;
    virtual ~TBase()                  = default;

            int get_n() { return n; }
    virtual int get_m() = 0;
    virtual void assign( const TBase* pBase ) { operator=( *pBase ); }
};
//---------------------------------------------------------------------------
class TChild3 : public TBase
{
  protected:
    int m;
  public:
    TChild3( int s, int p ) : TBase(s), m(p) {}
    TChild3(            const TChild3&  ) = default;
    TChild3(                  TChild3&& ) = default;
    TChild3& operator=( const TChild3&  ) = default;
    TChild3& operator=(       TChild3&& ) = default;
    virtual ~TChild3()                    = default;

    virtual int get_m() override { return m; }
    virtual void assign( const TBase* pBase ) override { 
      TChild3* pChild = dynamic_cast<const TChild3*>(pBase);
      if( pChild3 == nullptr ) throw false; // 例外を発生させる
      operator=( *pChild3 ); 
    }
};

TBaseのassignは使わないのであれば純粋仮想関数にしても良いだろう。

  TBase* s1 = new TChild3(2, 12);
  TBase* s2 = new TChild3(3, 13) ;

  s2->assign( s1 ); // s2->n=2, s2->m=12

関数名を適切に指定することで、何をしたいのかもよく判る。

namespaceに関する備忘(その1)

namespaceで名前空間を指定すると、同じ関数名でも別の名前空間であればコンフリクト(名前の衝突)しないので、プログラム作成の上で重宝する。
例えば、テンポラリーな処理を行う上でローカルな関数を定義することがあるが、関数名を定義しているのを忘れて、他の場所で同じ名前の関数を作成すると、ほぼ100%理解のできないバグとなって苦しめられる。

// file1.cpp
// 適当なローカルの作業関数
int calc( int x )
{
  return x * 2;
}

// 呼び出し元
void par()
{
  int x = 2;
  int y = calc(  x ); // file1.cpp、file2.cppで実装されているcalcのどちらかが呼ばれる
}
// file2.cpp
// 適当なローカルの作業関数2
int calc( int x )
{
  return 0;
}

// 呼び出し元
void par2()
{
  int x = 2;
  int y = calc(  x ); // file1.cpp、file2.cppで実装されているcalcのどちらかが呼ばれる
}

うかつに上のようなコーディングをしても、コンパイル及びリンクができ、大概の場合名前の衝突に関して警告もエラーも出ない。
そして、どちらかで関数が期待とは異なる値を返すことで理解のできない状況を作り出す。

このような事態の予防のために名前空間を使うことができる。

// file1.cpp
// 適当なローカルの作業関数
namespace file1 {
int calc( int x )
{
  return x * 2;
}
} // end of namespace file1

// 呼び出し元
void par()
{
  int x = 2;
  int y = file1::calc(  x ); // file1.cppで実装されているcalcが呼ばれる
}
// file2.cpp
// 適当なローカルの作業関数2
namespace file2 {
int calc( int x )
{
  return 0;
}
} // end of namespace file2

// 呼び出し元
void par2()
{
  int x = 2;
  int y = file2::calc(  x ); // file2.cppで実装されているcalcが呼ばれる
}

上のように名前空間で縛れば、コーディングした期待通りに動作する。
名前空間ではなくユニークな関数名、例えばローカルな関数はファイル名+関数名にするというルールでも同じ結果は得られるが、名前空間でうれしいのは下のように、名前空間を省略できることがある。

// file1.cpp
// 適当なローカルの作業関数
namespace file1 {
int calc( int x )
{
  return x * 2;
}
} // end of namespace file1

// 呼び出し元
using namespace file1; // 名前空間file1を使うことを宣言
void par()
{
  int x = 2;
  int y = calc(  x ); // file1.cppで実装されているcalcが呼ばれる
}

呼び出し元が増えると、タイプ量も大分変わるのでうれしい。
ただし、注意点として名前空間を使っていても、インクルード関係が無いなど別個に定義された全く同じ名前空間、関数名の場合はコンパイラーもリンカーもエラーや警告を出さないので、名前空間の定義には慎重になる必要がある。まあ、通常同一ファイル名の重複はできないのでファイル名を基に名前空間命名することが良いだろう。
クラス内関数からの呼び出しであれば、ローカル関数はクラスに定義した方が良い場合が多いのでそうすべきだろう。
頻繁に仕様が変わる可能性がある場合や、DLLのインターフェースなど関数の仕様を外部には出したくない場合には、こちらの方法をとるのが良いだろう。

2022-12-08 追記

上記でも可能だが、無名名前空間を用いればもっと簡単なことが判った。

// file1.cpp
// 適当なローカルの作業関数
namespace {
int calc( int x )
{
  return x * 2;
}
} // end of unnamed namespace 

// 呼び出し元
void par()
{
  int x = 2;
  int y = calc(  x ); // file1.cppで実装されているcalcが呼ばれる
}
// file2.cpp
// 適当なローカルの作業関数2
namespace {
int calc( int x )
{
  return 0;
}
} // end of unnamed namespace 

// 呼び出し元
void par2()
{
  int x = 2;
  int y = calc(  x ); // file2.cppで実装されているcalcが呼ばれる
}

無名名前空間は、そのファイルの中でのみ通用することがポイントだ。
かつ、using namespace ~を使わなくとも無名名前空間外で関数名を用いることができる。

TRichEditでリッチなテキストをプログラムする

TRichEditを使って、メモ帳もどきを作る場合は、編集ボタンなどでモードを切り替えするなどすれば、任意の修飾を行える。
一方、プログラムで文字列を修飾しようとすると気をつける必要がある。

修飾を前提とした場合やってはいけないこと

TRichEdit::Text、TRichEdit::Linesにアクセスして文字列を操作しない。操作するとその時点までの全ての修飾情報は消える。
次のようなコードを書くと、1つ目の修飾(1を上付き文字にする)が消える。

  RichEdit1->Lines->Add( u"012" );
  // 1を上付き文字にする
  RichEdit1->SelStart  = 1;
  RichEdit1->SelLength = 1;
  RichEdit1->SelAttributes->Offset = 4;
  RichEdit1->Text = RichEdit1->Text + u"345"; // ここで修飾情報がリセット
  // 4を下付き文字にする
  RichEdit1->SelStart  = 5; // 改行が1文字としてカウントされている
  RichEdit1->SelLength = 1;
  RichEdit1->SelAttributes->Offset = -3;

TRichEdit::Textは修飾情報を持たないので、今回の目的では使ってはならない。
なお、TRichEdit::Lines::Add、TRichEdit::Lines::Insert、TRichEdit::Lines::Deleteは使っても他の行の修飾情報を消さない。

修飾を行うプログラム

プログラム上では、TRichEdit::SelStart・TRichEdit::SelTextを使って文字列を追加・挿入し、修飾はTRichEdit::SelAttributesプロパティを用いる。
TRichEdit::SelStart・TRichEdit::SelTextを用いると、現在のキャレット位置に文字を挿入してくれ、入力後は入力した文字の直後に移動してくれるので便利だ。

  RichEdit1->SelStart = RichEdit1->GetTextLen(); // 現在のテキストの末尾にキャレットを移動であればこれでも良い
  TTextAttributes* pAttr = RichEdit1->SelAttributes;
  pAttr->Italic = true;
  RichEdit1->SelText = u"k";
  pAttr->Italic = false;
  pAttr->Size   = 7;
  pAttr->Offset = -3;
  RichEdit1->SelText = u"t=";
  RichEdit1->SelText = u"15";
  pAttr->Size   = 10;
  pAttr->Offset = 0;
  RichEdit1->SelText = u" = 2.6×10";
  pAttr->Size   = 7;
  pAttr->Offset = 4;
  RichEdit1->SelText = u"-3";
  pAttr->Size   = 10;
  pAttr->Offset = 0;
  RichEdit1->SelText = u" (cm/sec)\r\n次行";

TRichEdit::SelAttribultesは1つのステートしか保持できないが、追加される文字列の修飾はTRichEdit::SelAttribultesを参照してセットされる。
ここで気づかれると思うが、文字列を細切れでSelTextで追加すると、それぞれに修飾タグがひっつくので、リッチテキストのサイズが大きくなることになる。

注意事項

改行の取り扱い

TRichEdit中での文字の位置は改行コードを含む。
ただし、やっかいなのはTRichEdit::Text系(TRichEdit::GetTextLen()、TRichEdit::Text::Length()など)とTRichEdit::Sel~(TRichEdit::SelStart、TRichEdit::SelLengthなど)では改行コードの取り扱いが異なることだ。
TRichEdit::Text系は改行コードを2文字として計算する。一方TRichEdit::SelStartは改行コードを1文字で計算して指定する。
任意の位置の文字列を選択する場合で、TRichEdit::TextをPosで検索した場合は、上記を考慮して、改行コードをスキャンして改行コード分調整する必要がある。
コード中にも記載したが、単純に末尾にキャレットを移したいのであれば、TRichEdit::GetTextLen()の値をセットするだけで済む。
なお、プログラム中で、文字列に改行コード"\n"のみを埋め込んだ場合でも、自動で"\r\n"に変更される。

TRichEdit::SelAttributes

TRichEdit::SelAttributesはステートデータなので、自動で元には戻らない。
文字列を追加するときにはTRichEdit::SelAttributesを必ず自分の意図する値にセットしておく。

PILOTの顔料インク 強色(TSUWAIRO)

この4日にPILOTから顔料インクの強色(TSUWAIRO)が発売された。
知っている範囲ではPILOTの万年筆用顔料インクは初めてのはず。
国内大手3社(PILOT,SAILOR,プラチナ)の中では最後発となる。
同時にクリーニングキットも発売、こちらも最後発。

現在、自分がメインで使う万年筆のカスタムヘリテイジ91には、SAILORの顔料インクの蒼墨を入れて使っている。
大事な人への手紙を書くこともあるし、普段使いでもにじみの少ないインクを使いたかったからだ。
別に駄目ではないだろうが、いままでインクと万年筆のメーカー違いになんとなくモヤモヤしていたのは確かだが、PILOTから顔料インクの発売を特に待ち望んでいたわけでもなかった。
ともあれ、偶然発売を知って発売当日に顔見知りの文房具屋で取り寄せをお願いして手に入れた。

f:id:konishih:20220311140123j:plain
強色 BlueBlack 箱外観

何というか、シリーズ名や色の種類より「売り文句」の方が目立つ。どうですかね・・・

f:id:konishih:20220311140330j:plain
万年筆クリーニングセット

同時に入手した万年筆クリーニングセット。右側がチャックになっていて、使わないときにはしまっておけるのは便利だと思う。

f:id:konishih:20220311140537j:plain
クリーニングセット 裏面

使い方は他社とほぼ同じ、セットには洗浄液が5袋入っている。

f:id:konishih:20220311140713j:plain
左SAILOR蒼墨、右PILOT 強色BlueBlack

蒼墨との比較、蒼墨は50ml、強色は30mlだ。
PILOTの既往製品と同じ瓶だろうか? 少しクラシカルな感じがして、万年筆を使っているなぁと言う気になる。

f:id:konishih:20220311140939j:plain
色の比較

蒼墨と強色でそれぞれ書いてみる。写真は書いた直後のもの。
蒼墨はカスタムヘリテイジ91でニブはF、強色はコクーンでニブはMなので、文字の太さが違うことには目をつぶって欲しい。

強色の方が色味としては青が強い。濃さは同等か。

f:id:konishih:20220311141510j:plain
RHODIAのメモ帳でテスト

使った用紙はRHODIAのメモ帳。サイズ感が適当で方眼+ミシン目+リングとなるメモ帳は貴重で、かつ万年筆を用いても裏抜けがほとんど無い優れものだ。

10分ほど放置して、水を滴下してにじみを確かめた。

f:id:konishih:20220311141754j:plain
にじみの確認

わかりにくいかもしれないが、左側のメーカー名の上にまたがるようにどぼっと水滴を落としている。
どちらもほとんどにじまず、大変優秀な結果となった。

f:id:konishih:20220311143355j:plain
時間をおいたもの

時間をおいてみると、少し色味の傾向が変わってきた。強色は色の濃度がやや薄くなったように見える。
一方、蒼墨は黒みが強くなり墨染めの感じがする。
どちらも大変良い味を出しているが、強色の発色はオーソドックスで万人受けする感じだ。

中華系スパムが来なくなって怖い

大体1日に数通来ていた中華系詐欺メール。
メールソースのDate:を見れば+8:00となっていて、IPアドレスを確認しなくともほぼ確実に中国発のメールだと判る。
これが今月に入ってから1通も来ないので気持ちが悪い。
逆に-3:00とか-8:00,-9:00と言ったアメリカ大陸系のスパムがちらほら来るように。

ここまで極端だと中国が情報統制を大幅に強化したのではないかと勘ぐってしまう。
戦争の足音でなければ良いが。

Viewport3Dの画像をコピー・印刷するには

TViewport3Dの画像をコピーしたり印刷したい場合はどうするか。
その方法と注意点について示す。
まず、今回のコードはAndroidでは動かない(プリンターサービスとかがない)コードが含まれるので注意。

画像をコピー

TViewport3Dの画像をクリップボードにコピーするには、以下のようにする。

// あらかじめインクルード
#include <FMX.Platform.hpp>
#include <FMX.Clipboard.hpp>

void __fastcall OnCopy( TObject* Sender )
{
  TBitmap* pBmp = Viewport3D1->MakeScreenshot();
  TBitmapSurface* pBmpSurface = new TBitmapSurface;
  pBmpSurface->Assign( pBmp );

  _di_IFMXExtendedClipboardService pCs =    TPlatformServices::Current->GetPlatformService(__uuidof(IFMXExtendedClipboardService));

  pCs->SetImage( pBmpSurface );

  delete pBmp;
  delete pBmpSurface;
}

VCLとは異なり、TBitmapそのままでクリップボードに渡せないので、一回TBitmapSurfaceを介さなければならない。

印刷

TViewport3Dの画像を印刷するには、以下のようにする。

// あらかじめインクルード
#include <FMX.Printer.hpp>

void __fastcall OnPrint( TObject* Sender )
{
  TPrinter* pPrn = Printer::Printer();

  pPrn->BeginDoc();
    pPrn->Canvas->BeginScene();
      TRectF ARect(  dLeft, dTop, dRight, dBottom );
      Viewport3D1->PaintTo( pPrn->Canvas, ARect, nullptr );
    pPrn->Canvas->EndScene();
  pPrn->EndDoc();
}

プリンターへのポインターは上記のようにネームスペースつきにする必要がある。
VCLとは異なり、印刷中に描画コードはBegeinScene()とEndScene()で挟む必要がある。

注意事項

MakeScreenshot()で得たビットマップは不要になり次第削除する。

TViewport3D::MakeScreenshot()で得たビットマップは、利用者側で削除する必要がある。
削除しない場合はメモリー領域が徐々に圧迫されることと、Windowsリソースが消費されるのでGDIエラーが出る可能性がある。

TViewport3Dで生成される画像のサイズは、TViewport3Dのサイズ

TViewport3D::MakeScreenshot()はもちろん、TViewport3D::PaintTo()で得られる画像も基のサイズはTViewport3Dのサイズである。TViewport3D::PaintTo()では与えるTRectFのサイズに引き延ばしして描画される。

任意のサイズにしたい場合は、以下のようなテクニックを用いる。

#include <FMX.Printer.hpp>

void __fastcall OnPrint( TObject* Sender )
{
  int nOldWidth  = Viewport3D1->Width;
  int nOldHeight = Viewport3D1->Height;

  // サイズを指定
  Viewport3D1->Visible = false; 
  Viewport3D1->Width   = dW; 
  Viewport3D1->Height  = dH;
  
  TPrinter* pPrn = Printer::Printer();

  pPrn->BeginDoc();
    pPrn->Canvas->BeginScene();
      TRectF ARect(  dLeft, dTop, dRight, dBottom );
      Viewport3D1->PaintTo( pPrn->Canvas, ARect, nullptr );
    pPrn->Canvas->EndScene();
  pPrn->EndDoc();

  // サイズを元に戻す
  Viewport3D1->Width   = nOldWidth;
  Viewport3D1->Height  = nOldHeight;
  Viewport3D1->Visible = true;

}

但し、Viewport3Dで作成できる画像のサイズは限りがある(10000x10000と言った画像は作れない)ので、プラットフォームによるが概ね縦横5000ピクセル以上になる場合は、作成に失敗することを念頭に設計する必要がある。