C++

  • C++のリファレンスとしては、有志によるcppreference.comというwikiが最も人気がある。
  • cplusplus.com - Tutorials - Type conversions

volatile

  • 変数を宣言するときにconstまたはvolatileというtype qualifierを付けることができて、まとめてcv-qualifiersと呼ぶ。constはよく使うがvolatileは使わないので、考えなくていい。
  • 例えばある関数の中である変数が変更されていないとき、コード上で繰り返し変数を読むように書かれていても、コンパイラは最初に一度だけ読むように最適化することがある。しかし、組み込みやデバイスドライバの実装においては、その変数のアドレスに直接アクセスして状態を更新している場合があって、そのような最適化が行われることは不都合である。そのようなときにvolatileというtype qualifierを使う。よって特にそのような事態になったときのみ思い出せばいい機能である。
  • メンバ関数をcv-qualifyすることもできる。これについても、constは頻繁に使われるがvolatileは使われない。どちらにせよ変数*thisに対して作用する。また、関数の型の部分である。
  • 要するに、volatileについて考える必要はなく、cv qualifierと言われたらconstだと思っておけばいい。
  • コンパイラが最適化しない場合とアセンブリを比較するようなときに便利だという人もいる。

casting

  • C++では、4種類のキャスト?が定義されている。
    • static_cast
      • ほとんどの場合これを使う。
    • dynamic_cast
      • 子孫クラスの型にしたいとき使う。実行時に正当性が判定される。
    • const_cast
      • constを外すときに使う。普通はやるべきでないことなので、必要ない。
    • reinterpret_cast
      • 自由に型を変えたいときに使う。まず必要ない。
  • C言語のキャストの構文を使わずにこれらのみ使うべきだとされる。これらのキャストは、C言語の構文では明示的でなかった、キャストする意図を表す意義があるのだと言われる。
  • C++で、functional cast expressionというものが導入された。これによって例えば(unsigned)xunsigned(x)と書ける。どちらにせよC言語のスタイルのキャストであって使うべきではないらしい。static_castを使えばいいらしい。

NAND logic

#include <iostream>

// 真偽値を表す型。
class FBool
{
public:
    // 真偽値がvalueであるFBoolオブジェクトを生成する。
    FBool(bool Value)
    {
        this->Value = Value;
    }
private:
    // このFBoolの真偽値。
    bool Value;
    friend std::ostream& operator<<(
        std::ostream&,
        const FBool);
    // このクラスの内部の値を読めるのはNand関数だけとする。
    friend FBool Nand(
        const FBool,
        const FBool
    );
};

// FBool型オブジェクトを印字可能にする。
std::ostream& operator<<(
    std::ostream& Stream,
    const FBool Object)
{
    Stream << Object.Value;
    return Stream;    
}

// 与えられた真偽値xとyがともに真のときのみ偽である関数。
FBool Nand(
    const FBool X,
    const FBool Y)
{
    // Nand関数は所与に与えられるものとして生のブーリアン演算を用いる。
    if (X.Value && Y.Value)
    {
        return FBool(false);
    }
    else
    {
        return FBool(true);
    }
}

// 関数の型に別名をつける。
using FUnaryOperator = FBool (*)(
    const FBool);
using FBinaryOperator = FBool (*)(
    const FBool,
    const FBool);
// 単項演算子について、すべての入力パターンを出力する。
void Examine(FUnaryOperator Operator)
{
    FBool False = FBool(false);
    FBool True = FBool(true);
    std::cout << Operator(False) << ", ";
    std::cout << Operator(True) << "\n";
}
// 2項演算子について、すべての入力パターンを出力する。
void Examine(FBinaryOperator Operator)
{
    FBool False = FBool(false);
    FBool True = FBool(true);
    std::cout << Operator(False, False) << ", ";
    std::cout << Operator(False, True) << ", ";
    std::cout << Operator(True, False) << ", ";
    std::cout << Operator(True, True) << "\n";
}

FBool Not(const FBool X)
{
    // Nandの出力abcdの1110のうちbdを取れば10つまりNot演算となる。
    return Nand(X, X);
}

FBool And(
    const FBool X,
    const FBool Y)
{
    // NandはNot Andの意味だから、さらにNotすればAndになる。
    return Not(Nand(X, Y));
}

FBool Or(
    const FBool X,
    const FBool Y)
{
    // 出力がabcdである関数について入力XをNotすればcdabになる。
    // 出力がabcdである関数について入力YをNotすればbadcになる。
    // そのためXもYもNotすればdcbaになる。
    // よってNandつまり1110のXもYもNotすれば0111つまりOrとなる。
    return Nand(
        Not(X),
        Not(Y));
}

int main()
{
    Examine(Not);
    Examine(And);
    Examine(Or);
    // NotとAndとOrは使いやすいので、他の演算子も作れる。
}

// Output:
// 1, 0
// 0, 0, 0, 1
// 0, 1, 1, 1

POD (plain old data) type

POD (Plain Old Data)という型がある。PODとは、C言語の構造体とbitレベルで互換性があるデータ構造のことである。ABI (Application Binary Interface)互換性があるともいう。POD型の代表的な用途は、C言語とのバイナリデータのやり取りである。

PODというクラスを継承しているわけではないから、PODは型の型というべきものかもしれない。

任意の型がPOD型かどうかは、std::is_podで調べられる。C++の仕様のバージョンによって、POD型の定義は少し異なる。バージョンが上がるほどPODという言葉は使われなくなっており、C++20では廃止された。

現在では代わりに、trivial typeとstandard layout typeというものがある。両者を兼ね備えるものがPOD型である。

C++11以降では、std::is_podstd::is_trivialstd::is_trivially_copyablestd::is_standard_layoutが使える。このうちis_podはC++20で、deprecatedになった。これら4つは、同じ名前の型かどうかを調べるためのものである。何がPODかの厳密な議論は、細部があって複雑だ。理解を深めたい場合には、これら関数を起点に考えると便利かもしれない。

実用上は恐らく、PODの基本的な要件を守って型を定義し、これら関数で実際にPODにできているか確認すればいい。そのため、PODになるための詳細な条件を知っておく必要はなさそうである。そもそもPODについて考える必要のない場合が多そうである。しかし、定義によってPODでなくなる理由について学ぶことは、C++への理解を深めそうではある。

data structure alignment(データ構造アライメント)という言葉がある。そこにおいては、パッキングとパディングという言葉も使われる。4バイトでpackするとき、1バイトのメンバに対しては3バイトのpaddingが追加される。POD型だとは言っても、アライメントによってバイナリ表現は異なるだろう。しかし実用上は多くの環境で同じ結果が期待できるようだ。またそもそも、endiannessという論点もある。

std::endl

using namespace std;することは避けるべきだとされる。

std::cout << '\n';std::cout << std::endl;という似たものがある。後者はstd::cout << '\n' << std::flush;と同じ意味になる。

LinuxWindowsでは改行コードが異なるが、そのためのものではない。std::cout << '\n';としても、コンパイルした環境の改行コードが適切に用いられる。つまりWindowsでは"\r\n"が出力される。Windowsで\nを出力したいときにはファイルをバイナリで開く。

std::endlを、'\n'くらいの意味だと思っていると、少し大きなデータをファイルに書き込んだりするときに、速度が無駄に大きく低下するなどする。一方で、デバッグのための出力などにも\nを汎用してしまうと、エラー終了する直前に書かれたはずのログが読めない。

思うに、任意のコードについて、エラー終了する可能性はある。よって、日頃からflushを最小限にしていると、bufferingについて捨象つまり忘れて考えたときに、デバッグ時間が(起きたバグの本質と関係がない下らない理由で)伸びる可能性がある。一方で、flushによる速度の低下が問題にならない場合が大半だろう。ただし、flushによる速度低下が問題になる場合も少なくはない。改行は、flushの粒度としては悪くない。まさに、std::endlの設計思想もそこにあるのだろう。よって、std::endlを用いることを原則として、必要な場合にはそれを避けるべきことに思い至れるべきだろうと思う。「遅いなあ」と思ったときに、「そういえばflush」と思えればいいのであって、「遅いなあ」と思わないときにflushまで最適化すべきではないだろう。

std::ios:sync_with_stdio(false);というものがある。C言語を使うときには(必然的に)printf()などのみを使い、C++言語を使うときにはstd::coutなどのストリームのみを使うべきだとされる。よって行儀よく使うなら、sync_with_stdio(true)されている必要はないと考えられる。しかしながらもう1点、スレッドセーフという効果もあるらしい(?)。ならばその意味で、sync_with_stdio(true)されているデフォルトの状態で使いつづける合理性があるだろう。

std::cin.tie(nullptr);というものがある。これは、cinが実行される前にはcoutがflushされ、coutが実行される前にはcinがflushされる、という機能をオフにするものらしい。プロンプトを表示しておいてからユーザのキーボード入力を受ける場合など、オンになっていたほうがいい場合が明らかに多くある。競技プログラミング以外では、オフにする必要はないだろう。

Donald Knuthによる格言、「Premature optimization is the root of all evil.」は、大切な真実だ。しかし、プログラミングの実行時間を最短にしようという興味は、C++に触れていくなら大切な気がする。そもそも、多くの問題についてはPythonのほうが遥かに生産性が高そうだ。よって、積極的にflushしたいときだけflushすべきだという考え方もできる。その場合、std::endlは使わず、\nとstd::flushを常に陽に分けて書くスタイルも有意義な気はする。

copy constructor

任意のクラスインスタンスは必ずcopy constructorを持っている。copy constructorには2種類ある:

  • implicit copy constructor
  • user-defined copy constructor

intはオブジェクトである。

user-defined copy constructorがあるときには、rule of fiveの原則により、他のメンバ関数も定義されなければならない。

copy constructorのシグネチャは、T (const T& Object);である。

copy constructorというものとassignment operatorというものがあって、同じ「=」を書いて操作するので、少し紛らわしい。未初期化のオブジェクトに対する構文ではcopy constructorが使われ、初期化済みのオブジェクトに対する構文ではassignment operatorが使われる。

Assassin's Creed IV: Black Flag

2013年のゲーム。

f:id:Cpp:20201205093927p:plain

import matplotlib.pyplot as plt

items_to_plot = [
    ('Anne Bonny', 1697, 1782),
    ('Bartholomew Roberts', 1682, 1722),
    ('Benjamin Hornigold', 1680, 1719),
    ('Blackbeard', 1680, 1718),
    ('Calico Jack (John Rackham)', 1682, 1720),
    ('Charles Vane', 1680, 1721),
    ('John Cockram', 1689, 1719),
    ('Josiah Burgess', 1689, 1719),
    ('Laureano de Torres y Ayala', 1645, 1722),
    ('Lawrence Prince', 1630, 1717),
    ('Mary Read', 1685, 1721),
    ('Stede Bonnet', 1688, 1718),
    ('The Republic of Pirates', 1706, 1718),
    ('William Kidd', 1655, 1701),
    ('Woodes Rogers', 1679, 1732),
]

for i, item in enumerate(items_to_plot):
    label, begin, end = item
    x = [i, i]
    y = [begin, end]
    plt.plot(x, y, label=label, linewidth=4)

plt.legend()
plt.xticks([])
plt.savefig('png.png')
plt.show()

C++

C++は、1950年にデンマークで生まれたBjarne Stroustrup(ビャーネ・ストロヴストルップ)が1985年に作成したプログラミング言語である。仕様の標準として現在のところ5個の標準がある。それぞれの通称は、C++98、C++03、C++11、C++14、C++17である。

言語標準には、標準ライブラリも含まれている。標準ライブラリの一部として、Standard Template Library (STL)がある。STLは、1950年にロシアで生まれたAlexander Stepanov(アレクサンドル・ステパノフ)が開発した。

言語標準は、有料であって無料で公開はされていない。しかし、ほとんど等価な草案は公開されていて、そちらが広く利用されている。どの標準についてどの草案を参照すべきか、cpprefjpというウェブサイトに記載されているようだ。例えば、C++17の標準についてはDocument Number N4659をHTMLで閲覧できる。なお.pdfファイルは1,622ページある。

Hatena Blogでは、Markdown記法が使える。

const correctness

という言葉がある。

律儀にconstを多用すると、かえって面倒なことにならないだろうか? 例えば、class Tを変更しないメンバ関数void print() const;があるとする。しかし、もしprint()した回数を状態として持ちたくなったら、インタフェースの規約に実装が縛られて困るのではないだろうか? あるいは、キャッシュを保持したくなる場合もあるかもしれない。mutableキーワードというものがある。外部に公開しない内部の状態についてはこれを使い、外向きにはconstとする考え方もあるようだ。話が複雑になる気がする。

rule of five

rule of threeあるいはrule of fiveという言葉がある。

  • destructor
  • copy constructor
  • copy assignment operator
  • move constructor
  • move assignment operator

rule of zeroという言葉もある。

exception safety

という言葉がある。David Abrahamsが考案した。

  • No-Throw Guarantee
    • 最も高い安全性。ユーザにとっては正常系しか存在しないように見えるもの。
  • Strong Exception Safety
    • rollbackして元の状態に戻れることは保証されているもの。
  • Basic Exception Safety
    • 例外が発生したとしてもシステムを矛盾した状態にはしないもの。
  • No Exception Safety
    • 最も低い安全性。例外が生じた場合の状態について保証はない。

参考:

copy and swap idiom

という言葉がある。

Joint Strike Fighter Air Vehicle C++ Coding Standards

というものがある。Lockheed Martin社が2005年に定めたもの。F-35のために作られた。F-35より前はAda言語が使われていた。

move semantics

という言葉がある。C++11で導入された。右辺値参照型(T&&)が導入されたことによる。

従来でも、ポインタを陽に使えば同じ性能を出せる。所有権の操作を陰に書けるようにした。

C++には、garbage collectionの機能はない。RAII (Resource Acquisition Is Initialization)によることが原則であり、それで済まないときには、newとdeleteを対応させることを基本にしてきた。garbage collectionの方法には、大きく分けて、TracingとReference Countingがある。shared_ptrは参照カウント方式によるgarbage collectionであり、unique_ptrは、所有者が1つのときのそれだが、JavaのようなTracing方式のgarbage collectionは、言語仕様としては今なお持っていない。その意味で、C++にはgarbage collectionの機能はない。

move semanticsやsmart pointerは、ポインタなどによるboilerplate codeを抽象化して隠蔽し、外見をJavaPythonに似せる方向性なのではないかと思う。しかし、Tracing方式のgarbage collectionが存在しない以上は、参照カウントや所有権といった話になり、プログラマが気にかける事柄は必然的に存続する。

RAII

RAIIを使わずに、オブジェクトの生成と破棄を常にnewとdeleteでやると、C++の基本はずっと分かりやすいのではないだろうか。しかしそれではコードが煩雑になるためRAIIが導入され、連鎖的に、コピーコンストラクタ、さらにはデフォルトの暗黙的なコピーコンストラクタなどが定義された。そのため、smart pointerやmove semanticsが導入される以前から、コピーコンストラクタについてだけでも、C++は少し難しさのある言語になってしまっている気がする。

恐らく、C++の理解が浅いときに起こる問題は、動作がおかしくなるものと、動作が非効率になるものとに分けられるのではないだろうか? 例えばmoveで済むところで、大量のコピーを行っていれば、処理のパフォーマンスが下がるだろう。しかしとりあえずは気にせず、コードを書くことでC++の基本的な機能に慣れていくことのほうが大切だろう。