NEUROMANTIC

自分でC/C++/UE4/Graphics/ゲームなどをやったことをメモするブログ

Chapter 1 Memo (1)

1.5 Policies & Policy Classes

  • Policy Classes : Determine arbitary but specific small behavior, but different from dynamic run-time interface. Each Policy classes are bound to compound user-custom type as template on compile time. This class type have more importance to syntactic construct that should be valid, rather than which exact functions that class must implement.
  • Host Classes : That classes that use one or more policies. Host are responsible for assembling the structures and behaviors of their policies in a single complex unit.

1.5.1 Implementing Policy Classes with Template Template Parameters

  • テンプレートテンプレートパラメータを使用して、各Policyクラスの継承での特殊化を行うことも出来る。
  • Template Template Parameterを使う時には、見た目に騙されずに型パラメータが一致するテンプレートクラスの基本型だけを入れること。
template <template <typename> class CreationPolicy>
class WidgetManager : public CreationPolicy<Widget> {};

using MyWidgetMgr = WidgetManager<OpNewCreator>;
  • これを使って、同じPolicyを使用して別の型を特殊化したクラスで提供することもできるようになる。
// Library code
template <template <typename> class CreationPolicy>
class WidgetManager : public CreationPolicy<Widget> {
  // ...
  void DoSomething() {
    auto pW = CreationPolicy<Gadget>().Create();
    // ...
  }
};
  • Virtual Functionを使用することに似ているが、Policyは静的バインディングを使うので全てがコンパイルに処理される。そしてVtableなどの追加費用も費やさなくても済む。

1.5.2 Implementing Policy Classes with Template Member Functions

struct OpNewCreator {
  template <class TType>
  static T* Create() { return new TType(); }
};
  • Policyを作成するときに、Policyクラスに型引数をつけることではなくて指定したメンバー関数に型引数をつける方法もある。
  • しかしメンバー関数に型をつけるようとしたら、コード作成の維持補修が難しくなる。C++03以前のコンパイラーも支援するようにする長所もあるが、今どきとなってはこういうやり方をすすめるのは推薦できなさそう。

1.7 Destructors of Policy Classes

もしHostからPolicyへ型変換をするつもりであるなら、Policyで変換したものは絶対にPolicyのままで削除したりすることは出来ないようにしなければならない。そのため、こういうコードは禁じられている。

MyWidgetManager wm;
PrototypeCreator<Widget>* pCreator = &wm;
delete pCreator;
  • 最後の行の実行によるUBを防止する為にはPolicyクラスのDestructorをVirtualにする方法もあるが、そうするとVtableが生じてしまう。
  • この本ですすめる方法は、DestructorをProtected以下の権限を持つようにしてPolicy型のインスタンスを削除できないようにコンパイルに任せることである。
template <class T>
struct OpNewCreator {
  static T* Create() { return new T; }
protected:
  ~OpNewCreator() {}
};

1.8 Optional Functionality Through Incomplete Instantiation

  • クラスTemplateのメンバー関数がコードで使わなかったら、そのメンバー関数はそのコードが特殊化した型によって結果的に正しいコードになるかを確認しない。

例えて、以下のようなコードがあるとする。

template <template <class> class CreationPolicy>
class WidgetManager : public CreationPolicy<Widget> {
  // ...
  void SwitchPrototype(Widget* pNewPrototype) {
    CreationPolicy<Widget>& myPolicy = *this;
    delete myPolicy.GetPrototype();
    myPolicy.SetPrototype(pNewPrototype);
  }
};

上記のコードは使い方によって3つの分析結果に分かれる。

  1. もしCreatorが型をPrototypeを支援する型として特殊化したら、Specializedした型はSwitchPrototype関数を使える。
  2. もしCreatorが型をPrototypeを支援すない型として特殊化してSwitchPrototype関数を使うとそのSwitchPrototype関数はコンパイラに分析され、そいてコンパイルエラーを吐き出す。
  3. もしCreatorが型をPrototypeを支援すない型として特殊化してSwitchPrototype関数を使わなかったら、プログラムはコンパイルが通せる。

1.9 Combining Policy Classes

template<
  class TType, 
  template <class> class CheckingPolicy, 
  template <class> class ThreadingModel>
class SmartPtr;

using SafeWidgetPtr = SmartPtr<Widget, EnforceNotNull, SingleThreaded>;
  • 上のコードのようにPolicyを適切に組み合わせして、モジュール化した効率が良いコードが書けることができる。

1.10 Customizing Structure with Policy Classes

  • データを貯蔵するものさえPolicyで変換できる。たとえば、SmartPtrはPointerをセーブするためにあるが、他の実装体ではHandleで処理することもある。
template <class T> class DefaultSmartPtrStorage {
public:
  using PointerType = T*;
  using ReferenceType = T&;
protected:
  PointerType GetPointer() { return ptr_; }
  void SetPointer(PointerType ptr) { ptr_ = ptr; }
private:
  PointerType ptr_ = nullptr;
};

そしてSmartPtrはこの様に実装する。

template <
  typename TType,
  template <class> class CheckingPolicy,
  template <class> class ThreadingModel,
  template <class> class Storage = DefaultSmartPtrStorage>
class SmartPtr;

1.11 Compatible and Incompatible Policies

  • 一つのPolicyで派生した様々のPolicy Classの間にも適切に変換関数を用意しなければいけない。
  • Policyのクラス間の変換を効率よくする方法とは、Policyごとに変換ができるように実装することである。
  • Policyのクラス間の変換をよりよくする為には変換するクラスを元としたConstructorか、Conversionオペレーターを実装する方が望ましい。
  • だとしても、Policyクラス間の変換には適切な政策を考えて実装すべき。

1.12 Decomposing a Class into Policies

  • とあるクラスで複数の行動があるとしたら、その行動が複数で処理できるようになればこの行動はPolicyの適用が出来るデザインを持つ。
  • typedefまたはusingを積極的に使うべき。
  • Policyを実装するときには、他のPolicyまたはサイドエフェクトを起こさないように、即ちお互いに垂直的の実装政策を持つこと。
  • 垂直的ではないPolicy政策を実装しようとしたら、そのお互いでの連絡手段を実装しなければいけない。だとしてもそうすることによって複雑度が増しになる。

例を挙げて、ArrayというPolicyがあるとする。このポリシーはSmartPtrが配列を持っているか否かを確かめる為にある。

template <class TType>
struct IsArray {
  TType& ElementAt(TType* ptr, unsigned int index) noexcept {
    return ptr[index];
  }
  const TType& ElementAt(TType* ptr, unsigned int index) const noexcept {
    return ptr[index];
  }
};

template <class TType> struct IsNotArray {};

しかしこのポリシーには問題が潜在する。ポリシーによってdelete[]をするかdeleteをするかがサイドエフェクトとして存在してしまう。この削除を行うポリシーをDestructionだとしたら、このArrayDestructionはお互いに垂直的でなくなってしまう。

  • 結論を言うと、垂直的でPolicyを作って、そうでなくなければならない場合にはお互いの通信の為にBoolean整数を利用して伝送しかねない。

Overall

#include <cstdlib>
#include <type_traits>
#include <memory>

struct OPNewCreator {
    template <class TType>
    static std::unique_ptr<TType> Create() { return std::unique_ptr<TType>(new TType); }
};

struct MallocCreator {
    template <class TType>
    static std::unique_ptr<TType> Create() {
        void* buf = std::malloc(sizeof(TType));
        if (buf == nullptr) { return nullptr; }
        else                { return std::unique_ptr<TType>(new(buf) TType); }
    }
};

struct SmartNewCreator {
    template <typename TType>
    static std::unique_ptr<TType> Create() { return std::make_unique<TType>(); }
};

struct CompileValidityCheck final {
    template <typename...> struct DetachedParam {};
    template <template <typename TParam1> class TPolicy, typename TParam1>
    struct DetachedParam<TPolicy<TParam1>> {
        using TParamType1 = TParam1;
    };

    template <typename CompoundCreationPolicy>
    static constexpr bool IsSatisfiedCreationPolicy() {
        using TParamType1 = typename DetachedParam<CompoundCreationPolicy>::TParamType1;

        return false 
        || std::is_same_v<OPNewCreator<TParamType1>,CompoundCreationPolicy> 
        || std::is_same_v<MallocCreator<TParamType1>, CompoundCreationPolicy> 
        || std::is_same_v<SmartNewCreator<TParamType1>, CompoundCreationPolicy>;
    }
};

struct Widget {
    Widget() = default;
    int operator()() { return 997; }
};

template <class CreationPolicy>
class Manager : public CreationPolicy {
    CompileValidityCheck::DetachedParam<CreationPolicy> TPolicyParam;
    static_assert(
        CompileValidityCheck::IsSatisfiedCreationPolicy<CreationPolicy>, 
        R"(Can not create class that not satisfied "creation policy")");
};

int main() {
    Manager<OPNewCreator<Widget>> widgetManager;
    
    auto widget = widgetManager.Create();
    const auto returnValue = (*widget)();

    return returnValue;
}