ぷろみん

プログラミング的な内容を扱ってます

C++17からはwstring_convertが非推奨となるので環境毎のコードが推奨される

概要

推奨されるマルチバイト文字変換。

非推奨

C++17からスタンダードライブラリのものは非推奨になる

cpprefjp.github.io

Visual Studioで下記のようなコードをC++17を有効にした状態で書くと

#include <codecvt>
#include <locale>

int main()
{
    std::wstring_convert<std::codecvt_utf8<wchar_t>, wchar_t> converter;
}

エラー C4996 'std::codecvt_utf8<wchar_t,1114111,0>': warning STL4017: std::wbuffer_convert, std::wstring_convert, and the header (containing std::codecvt_mode, std::codecvt_utf8, std::codecvt_utf16, and std::codecvt_utf8_utf16) are deprecated in C++17. (The std::codecvt class template is NOT deprecated.) The C++ Standard doesn't provide equivalent non-deprecated functionality; consider using MultiByteToWideChar() and WideCharToMultiByte() from <Windows.h> instead. You can define SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING or SILENCE_ALL_CXX17_DEPRECATION_WARNINGS to acknowledge that you have received this warning.

上記のようなエラーが出る。

推奨

それをふまえて、windows環境で推奨されるマルチバイト変換は下記のようになるだろう。

stackoverflow.com

std::basic_string::dataメソッドの知名度が低い。

C++の難しめな用語集

概要

先日、言いたい用語がすぐに出てこなかったので備忘録を作った。私が学習するのが遅かったり難しかったものを集めた。
その特性から多くは語らないのでググって。

Return Value Optimization (RVO)

  • Named Return Value Optimization(NRVO)

データ指向設計 (Data Oriented Design)

  • キャッシュライン

Perfect Forwarding

引数と全く同じ型を伝搬させるという説明が一番分かりやすかった。

universal reference

  • Rvalue
  • Lvalue

Template MetaProgramming(TMP)

メインの機能はconceptに代替されそう。

  • Substitution Failure Is Not An Error(SFINAE)

Type Erasure

Argument Dependent Lookup(ADL)

安易にusing namespaceしてると引っかかるやつ。

unqualified name look up

Unqualified name lookup - cppreference.com

One Definition Rule(ODR)

  • ODR違反
  • odr-use
  • リンケージ

Application Binary Interface(ABI)

C++ではないが。

Most vexing parse

レイアウト

  • Plain Old Data(POD) C++20からtrivial classとして扱う
  • standard-layout class
  • trivial class

Rust

Rustも同じレベルのプログラミングができるので、Rustの概念を用いてC++の概念を説明しても良いはず。

  • LifeTime
  • Ownership

規格

C++er は“合法”だとか“違法”だとか言いたくて仕方がないけれど、結局どういう意味? それより適合・適格・○○動作・○○規則・診断不要いろいろの関係が謎

ガイドライン

CppCoreGuidelines

  • Guideline Support Library (GSL)

VisualStudio2017はC++17に対応しているがデフォルトでは無効になっている

C++17を有効にする

プロジェクト > プロパティ > 構成プロパティ > C/C++ > 言語 > C++言語標準
から設定できます。

せっかくMSが標準まで追いついてくれたのに有効にしないともったいないです。

おまけ

上記を言いたかっただけなんですが、あまりに短かったのでVisualStudioで私が使う設定を書いておきます。

ツールバーにあるソリューション構成ドロップダウンリストの幅変更

デフォルト幅だとUnreal Engineのビルドをするとき等にソリューション構成名が長すぎて今何ビルドしているか分からなくなるので設定してます。
用語が限定的過ぎてググラビリティがやたら低くて毎度見つけるのに苦労します。

srz-zumix.blogspot.com

後、さっき調べて知ったんですがテキストが編集可なのがコンボボックスで編集不可なものがドロップダウンリストらしいです。
同じものを指す用語かと思ってました。

ClangFormat

コーディングスタイルを設定できます。
Visual Studioデフォルトのフォーマットが気に入らないなら変更しましょう。
独自のコーディングスタイルを選択するよりはデフォルトフォーマットを選択した方が良いとは思いますが。

ツール > オプション > テキストエディター > C/C++ > 書式設定 > 規定の書式スタイル

メンバ変数の色変更

Visual Studioのハイライトは優秀で言語内部の概念に対しても色を変えることができます。
しかしビルドに依存するのでUEぐらいの複雑なプロジェクトになるとハイスペックPCでも1分くらいハイライトが消えることがあります。

メンバ変数はコード上だと全くの宣言なしでいきなり登場するので視認性を上げると心が落ち着きます。

ツール > オプション > 環境 > フォントおよび色 > 表示項目 > C++フィールド

Chromeとかで使われているグラフィクスライブラリSkiaのWindowsビルド

手順

git clone https://github.com/google/skia

masterブランチだとWindowsに優しいものが入っていないのでtagを使います。

git checkout -b dev refs/tags/chrome/m39_2138

上記タグのコミットだとリポジトリルートにmake.batが存在するので、こちらをVisual Studio環境変数を適用した上で実行するとビルドできます。
環境変数の準備は一般的に面倒なので設定済み開発用コマンドプロンプトを利用すべきでしょう。
VSのインストールパスの下記バッチファイルを起動させたり、ウィンドウズアイコンのインストールしたアプリから呼ぶこともできます。

VisualStudio/Common7/Tools/VsDevCmd.bat

ただ1つだけ罠があって、いくつかのファイルがBOMなしutf-8で保存されているためビルドが失敗します。
私はそれらのファイルにBOMを追加することで対応しました。

最初はファイルエンコードを一致させることで解決しようと思ったのですが他のファイルにvimで:set fenc?としても
fileencoding=としか返してくれず諦めました。読み取れなかったらこうなるんですかね。

参考

qiita.com

この記事によりBOMなしutf-8が原因だと気付けました。ありがとうございます。

std::vectorのようなコピーコストの高いクラスに参照を使ってはいけないシチュエーション

概要

RVO(Return Value Optimization)の話です。特別に触れませんがNRVO(Named RVO)も最適化されるものとして話を進めています。

昔の効率の良いコード

void Initialize(std::vector<int>& buffer) {
    // bufferに色々な計算結果を詰める
    buffer.emplace_back(5);
}

昔は新しいデータ列を作成する際、アロケートの際には上記のような関数が使われていました。
それ故に下記のような分かり難い記述が散らかることになります。

std::vector<int> buffer;

// これくらいならまだ良いけど
Initialize(buffer);

// 生成に必要な引数が増えると何がなんだか
Create(buffer, elementA, elementIndex, combineBuffer);

// 使う
Use(buffer);

しかし、これには仕方ない事情がありました。
std::vectorを返すのがスマートだと分かっていても、そんな大きなものを返すとコピーコストが高いことが予想されるからです。

今の効率の良いコード

// 引数によってbufferが生成されていることがはっきり分かる
auto buffer = Create(elementA, elementIndex, combineBuffer);
// 使う
Use(buffer);

ここでRVOを知らない人はbufferの型がstd::vectorというのを見てびっくりします。
関数を見てみましょう。

std::vector<int> Create(...) {
    std::vector<int> result;
    // 色々な計算結果を詰める
    result.emplace_back(5);

    return result;
}

確かにRVOが効かなかった昔のコンパイラならば最悪のコードです。膨大なバッファのコピーが無意味に走る。
しかし、今の私たちにはRVOがあります。
上記コードは以下のコードと同等と解釈されます。

std::vector<int> buffer;

// 色々な計算結果を詰める
buffer.emplace_back(5);

// 使う
Use(buffer);

つまり、バッファポインタのコピーコストや関数の呼び出しコストすらも無く同様の処理を記述できるようになったのです。
昔のコードよりも高速に処理される上に遥かに高い可読性を持つこの初期化方法は想像以上に多くの可能性を私たちに提供します。

まず、コピーコストを無視できるので何らかのクラスを生成している箇所を機械的に分離できるようになります。
長い関数は大抵途中でなんらかのデータ列を生成していることが多いので、その部分だけでも分離することで効率を落とさずに可読性を上げることができます。

もうすでにRVOを行わないコンパイラは少ないと思っています。
加えて、最近の規格ではRVOを保証するものも出て来ているので上記のような書き方をしない理由はないと思います。

3Dプログラマのための数学

概要

画像付きで詳細に解説しようとしたらモチベーションが足らなかったので下書きをブログで。
Qiitaとかだと数学記号使えるので恰好良く書ける反面書くの面倒。

内積

3Dプログラミングで一番出てくるやつ。
何故大人気かというと三角関数の中で一番高速で計算できるから。

内積

float DotProduct(const float3& a, const float3& b) {
    return a.x * b.x + a.y * b.y + a.z * b.z;
}

のように足し算と掛け算のみで計算できるので速い。
ちなみに引き算は問題ないが割り算は他の演算と比べるとちょっと遅い。
それでもcos関数とかsqrt関数と比べるとマシ。
一般的な認識はそんな所だと思うけど、最近のGPUやCPUは進化しているのでnvidiaの出している情報を見ると、この関数は1命令でいける、これは4命令かかるとかが書いてあったりするので決まった環境での最適化はもうかなり特化した人じゃないと難しそうです。
まー、今はモバイルも強いので上記の一般常識に従う方向でも良いでしょう。

話がそれました。
内積

DotProduct(a, b) = aベクトルの長さ * bベクトルの長さ * cos(aベクトルとbベクトルの作る角度);

とも表すことができるので、aベクトルとbベクトルが単位ベクトル(長さが1のベクトル)の場合aとbベクトルのcosが取得できます。
またaベクトルのbベクトル成分が欲しい場合はaベクトルとbの単位ベクトルの内積をとってやればaの長さ * cosとなり取得できます。
この操作は射影(Projection)とも呼ばれaベクトルからbベクトルに垂線を引いた時の長さに相当します。
3Dでこの情報が欲しい場合の例としては法線ベクトルや接線ベクトル成分を取得したい場合等があります。

長さを求めるのは大変

長さを求めるためにはsqrt関数を使わないといけません。これは他の演算と比べると重いので使いたくありません。
なので長さの2乗を使います。長さの2乗は

return x * x + y * y + z * z;

で取得できるので高速です。
長さの2乗を計算する関数が用意されていない環境ではDotProduct(a, a)のように内積で代用したりします。

ベクトルは長さをもっている

ベクトルaを考えるとき、aベクトルは

aベクトルの長さ * aベクトルの単位ベクトル

と考えることもできます。

ここから一切の特殊な演算を使わずにaベクトルのbベクトル成分ベクトルを求めることができます。

aベクトル * DotProduct(a, b) / DotProduct(a, a)

上記演算の式を整理すると

aの単位ベクトル * bの長さ * cos

となり、aとbの長さを求めていないのにaとbの長さが必要な演算ができてしまうのです。
単位ベクトルを求めるためにはaベクトル / aベクトルの長さを計算する必要がありますからね。

外積

外積は計算上は

return float3(a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x);

これはやはり引き算と掛け算のみなので高速です。
しかし、演算結果にベクトルを含んでしまうので扱いにくいです。
意味としては

aの長さ * bの長さ * sin(aとbの作る角) * aとbが作る面の法線単位ベクトル

で表されます。 sinとcosは相反するほぼ同じ意味を持つので、扱いにくい外積よりも内積を使うというわけですね。

最後に

はい、「3Dプログラマのための数学」なんて言っておきながら足し算と引き算と掛け算しかしませんでした。

数学関数を高速動作させるやべーやつも研究されていますが、内積で解決できる問題は内積で解決するのが一番速いです。 github.com

C++コーディングガイドライン哲学1

概要

https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md
の「P: Philosophy」の感想や補足です。

アイディアを直接コードで表現する

例としてStrong typeやfor文の代わりにアルゴリズムを使うことを提案しています。
また、constを一貫して使うことやキャストによる型システム破壊の防止について言及しています。

Strong type

プリミティブ型のような汎用性の高い型は間違いの元なので、間違った操作ができない型を使おうという発想です。

IndexA indexA(10);
IndexB indexB(5);
A a;
B b;
a[indexA]; // OK
a[indexB]; // コンパイルエラー
if(indexA == IndexA(10) && indexB == IndexB(5){} // OK
if(indexA == 5 && indexB == 10){} // コンパイルエラー

Indexが全部int型だと必ずミスをします。自分は良くても後で保守する人は必ずミスをします。

スタンダードライブラリにある機能を再発明しない

これは簡単なようでいてかなり難しいです。これを正しく運用するためにはスタンダードライブラリの全機能を把握する必要があるからです。無理です。
なので、よくある処理を書く前にライブラリにないかな?といったんググるだけで良いと思います。
良く使いそうな例を少し挙げておきます。

一般的にあって欲しいfilterとmap(LinqだとWhereとSelect)もあるといえばあるけど・・・状態なので結局作らなければいけないことも多いです。
boostまで考慮に入れるともう少し便利になります。

  • std::remove_ifとcontainer::erase
    条件を満たした要素の配列を取得できます。
    参照配列を弄って欲しくない時はコピーするしかない?

  • std::transform 指定した方法で変換された配列を取得します。
    変換後結果を詰めた配列を返すものということは知っておいた方が良いでしょう。 Boost.Rangeをパイプライン記法のままコンテナに変換 - Qiita

for文でこの処理を書くとかなり蛇足感が出ます。

  • std::all_of 全ての要素が条件を満たすか確認します。

  • std::any_of どれか1つでも条件を満たす要素があるか確認します。

algorithmぐらい頻繁に使うがnumericにあるので存在を忘れやすい

  • std::accumulate 総和を求める

constを一貫して使う

constは伝搬されるものなので最初から心がけていないと「あれ?この関数にconst変数渡せないぞ」となってしまい、constを使わない文化が蔓延してしまうので早めに取り組んだ方が良いでしょう。
constに気をつける箇所はたくさんあります。

class Input {
public:
  // メンバ関数のconst
  Arguments GetArguments() const {
    return arguments;
  }

private:
...
  // メンバ変数のconst
  // コンストラクタで初期化しないとエラーとなるので使わない人も多いですが、const性を持つのなら定義すべきです
  const int version;
};

// 引数のconst
void Function(const Input& input) {
  // ローカル変数のconst
  // 蛇足っぽく見えるので嫌う人も多いですがconstを常に使う方が哲学に沿います
  const auto& arguments = input.GetArguments();
}

キャストした際にはそれがキャストされた値だと明示する

おそらくstd::anyやstd::variantについて言及しているのだと思います。
別の型として汎用管理しても良いけど元の型を忘れちゃダメ。

ユーザ定義リテラル

例として下記のようなコードが提示されています。

change_speed(23m / 10s);  // meters per second

何の説明もなく出現していますが、これは疑似コードではなくユーザ定義リテラルです。
ユーザー定義リテラル - cpprefjp C++日本語リファレンス

なにかと評判の良くないユーザ定義リテラルですが、ガイドラインで利用されていることを考えると使っていった方が良いのでしょうね。

ISO Standard C++を使う

理由は移植性だと思うのですが、このガイドラインはISOのために書かれているからISO Standard C++を使おうってなってます。
移植性では納得しない人が多かったのでしょうか。
まー、守ることは環境依存な拡張機能は使わないようにしようってことです。

意図を表現しよう

例えば配列の各要素について処理したい場合はwhileではなくforを使おうということです。
上記ぐらいは多くのプログラマーが実施できているとは思いますが、Read Writeの意図を明示できている人は思っているよりも少ないです。

// 参照のみすることを明示する
for (const auto& x : v) {}
// 書き換えを行うことを明示する
for (auto& x : v) {}

また、プリミティブ型は曖昧になりがちなので意味を表現した型を使うことを勧めています。

// 各パラメータが何を指しているのか分かり辛い
draw_line(int, int, int, int);
// 分かりやすい
draw_line(Point, Point);

最後により良い互換機能が提供されているものについてはそちらを使いましょう。

  • 単純なforはrage baseのforに
  • ポインタとその長さの組み合わせによる範囲指定はspanに spanとはGuideline Support Library(gsl)に定義されるクラス。MSによる実装が下記
    GSL/span at master · Microsoft/GSL · GitHub
  • あまりに長いスコープ変数 ネストが深くなったりスコープが長くなり過ぎる場合、意味単位に分割してクラス化や関数化を行い、スコープはせめて1画面以内にするのが望ましいと私は思います。
  • 生のnewやdeleteはスマートポインタやmake_shared、make_uniqueに
  • 関数の引数にプリミティブ型が多い場合は意味を表現した型を使うように

理想的には、プログラムは静的型安全であるべき

クラッシュやセキュリティの脅威となる既存の方法への代替案として下記を提案しています。

  • union(共用体)の代わりにstd::variant
  • キャストの代わりにtemplate
  • 部分配列の利用にポインタやインデックスを使うのではなくgsl::span
  • 範囲外アクセスエラーもgsl::spanなら検出できる
  • キャストするとしてもgsl::narrowやgsl::narrow_castを使おう narrowはnarrow<IndexA, IndexB>でIndexAをIndexBにstatic_castした値が入っているよと明示するクラス

実行時チェックよりもコンパイル時チェックの方が良い

間違ったコードはコンパイル時にエラーを出し、決して実行時までエラーを遅延させてはいけない。
また、static_assert等で検証した事項を実行時に再検証してはいけない。

コンパイル時にチェックできないものは、実行時にチェックできなければならない

上記タイトルだと幅広そうですけど、ようするにレンジアクセスにポインタとインデックスは使わずにgsl::span使おうぜって内容です。

実行時エラーは早めに捕捉しよう

nullポインターに代表される多重チェックをしないためにどうするべきか。

nullチェックが必要なポインタの代わりに参照を使うように、境界値チェックが必要な配列とレンジを指定する代わりにspanを使うようにチェックが終わって安心して使えることを明示する型を利用するようにしましょう。ということだと思います。次の項目で出てくるRAII的な考え方をエラーチェックにも適用しようということでしょう。

おわりに

これで哲学の項目の半分くらいです。続きは気が向いたら書きます。