ぷろみん

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

C++20時代のポリモーフィズム

概要

ポリモーフィズム良いですよね。
でも、継承嫌ですよね。
MixInみたいな綺麗な継承が使える場面も現実的なサービスを考えると難しい。
そこで簡単に追加、削除ができ可読性も悪くないstd::variantとconceptを使ったC++20時代のポリモーフィズムを紹介します。

コード

#include <iostream>
#include <vector>
#include <string>
#include <variant>

struct A{};
struct B
{
    std::string ToString()
    {
        return "B";
    }
};
struct C{};
struct D{};
struct E{};

using VariantType = std::variant<A, B, C, D, E>;

template<class T>
concept bool Printable = requires(T t){
    t.ToString();
};

void PrintIfPrintable(VariantType variant)
{
    std::visit([](auto&& arg)
        {
            if constexpr (Printable<decltype(arg)>)
            {
                std::cout << arg.ToString() << std::endl;
            }
        }, variant);
}

int main()
{
    std::vector<VariantType> variants;
    variants.emplace_back(A());
    variants.emplace_back(B());
    variants.emplace_back(C());
    variants.emplace_back(D());
    variants.emplace_back(B());
    variants.emplace_back(E());

    for(const auto& variant : variants)
    {
        PrintIfPrintable(variant);
    }
}
// 出力
B
B

visitがやや複雑なのを除けば素直な実装なのではないでしょうか。

variant

variantは状態として特定の型を持つという非常にポリモーフィズムと親和性の高い特徴を持っています。
現在の型にアクセスするためにはstd::visitを使わなければならないという制約は付くもののvariantで定義した型全てに同様の処理を加えることが簡単にできます。
また、継承によるポリモーフィズムにありがちな、このインターフェース継承しているクラスが見つからない問題を解決できることは素晴らしいです。
継承しているクラスは定義ジャンプで到達できないので大規模なプロジェクトだと数十秒かかる検索で探さないといけません。

concept

上記コードはWandboxで動作します。
Compiler optionsに-fconceptsを付けるのを忘れずに。
コンセプトの強さはこの雑に使い方書いたら動くという所にあります。
かなり実務よりな機能なので早く標準に入って欲しいです。

vcpkgでinstallするとCould not locate a complete toolsetとか言われる。

概要

Warning: The following VS instances are excluded because the English language pack is unavailable.
    C:\Program Files (x86)\Microsoft Visual Studio\2017\Community
Please install the English language pack.
Could not locate a complete toolset.

解決策

素直に書いてある通りに新しくインストーラーを持ってきて英語の言語パックを入れましょう。
ビルドチェーンが言語パックに依存しているの何とかならないんですかね。500MBくらいあるし。

その他

最近流行りのgrpcとかも難なくビルドできるし、思ったよりも良さげになってきましたね。vcpkg
googleのprotobufとかもウィンドウズならvcpkg使えよって感じになってきているし期待しても良いかもしれません。
少なくともgypよりはるかにビルド簡単なので素敵です。
自分のプロジェクトを組み込むのはまだ難しそうですが。

C++17から導入される範囲 for ループの制限緩和で何が変わるか

概要

C++17で範囲forループのbeginとendで違う型を指定できるようになりました。
範囲 for ループの制限緩和 - cpprefjp C++日本語リファレンス

これによって何が変わるのでしょうか。

イテレータの思想の違い

STLイテレータはメモリはコンテナが持ってイテレータは最小限の値のみ持つという思想で作られていました。
一方マイクロソフトの提供するAPIイテレータ自体が終端の判断をする機能を持つことが多いです。

その思想の違いをこの制限緩和は扱いやすくしてくれます。

親プロセスの名前を取得するサンプル

#include <iostream>
#include <optional>
#include <Windows.h>
#include <TlHelp32.h>

struct SelfOperationTag {};

class ProcessEntryIterator {
public:
    ProcessEntryIterator()
    {
        processEntries = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
        if (processEntries == INVALID_HANDLE_VALUE)
        {
            result = false;
            return;
        }

        processEntry.dwSize = sizeof(PROCESSENTRY32);
        result = Process32First(processEntries, &processEntry);
    }

    ~ProcessEntryIterator()
    {
        CloseHandle(processEntries);
    }

    const PROCESSENTRY32& operator*()
    {
        return processEntry;
    }

    void operator++()
    {
        result = Process32Next(processEntries, &processEntry);
    }

    operator bool() const
    {
        return result;
    }

private:
    HANDLE processEntries;
    PROCESSENTRY32 processEntry;
    bool result;
};

bool operator!=(const ProcessEntryIterator& iterator, SelfOperationTag)
{
    return iterator;
}

class Toolhelp32Snapshot {
public:
    auto begin() const
    {
        return ProcessEntryIterator();
    }

    auto end() const
    {
        return SelfOperationTag();
    };

    static std::optional<PROCESSENTRY32> Find(DWORD processID)
    {
        for (const auto& processEntry : Toolhelp32Snapshot())
        {
            if (processEntry.th32ProcessID == processID)
            {
                return processEntry;
            }
        }
        return std::nullopt;
    }
};

int main() {
    auto currentProcessID = GetCurrentProcessId();
    auto currentProcess = Toolhelp32Snapshot::Find(currentProcessID);
    if (currentProcess)
    {
        auto parentProcess = Toolhelp32Snapshot::Find(currentProcess->th32ParentProcessID);
        if (parentProcess)
        {
            std::wcout << parentProcess->szExeFile << std::endl;
        }
    }
}

煩雑なエラーハンドリングで抜けが出てしまいにくいのではないでしょうか。下記サイトでは現実的には起こらないでしょうがProcess32Firstのエラーハンドリングが抜けています。
モジュールの列挙 (PSAPI)

正しくハンドリングしてgotoやdo whileが出てくるのも辛いものです。
Windows-classic-samples/shared.c at master · Microsoft/Windows-classic-samples · GitHub

Visual Studio2017にVsVimをインストールすると上スクロールがバグる

概要

新しい環境にVisual Studio2017とVsVimをインストールすると上スクロールの際に期待しない動きをすることがある(2/3回)

バグの再現させ方

文字の折り返しが起こっているスクロールできるファイルでGHと移動すると私の環境では画面一番上の行にカレント行が移動し、カレント行が表示画面の一番下に行く。

暫定的な対処

.vimrcに下記を追加。

nnoremap H Hzt

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++フィールド