2020年6月25日木曜日

STLのtype_traitsを実装する その2

STLのtype_traitsを実装する その1の続き.

conjunctionを上手く実装できないか試行錯誤していて,たぶんこれでOKというのが出来たので書いておきます.

conditionalおよびconditional_tの実装

conditionalは,型のif文だと思えば良いでしょう.boolと型Tと型Fを渡すと,boolがtrueのときはtypeとしてTを,falseのときはtypeとしてFが手に入るわけです.

template <bool, class T, class F>
struct conditional
{
    using type = F;
};

template <class T, class F>
struct conditional<true, T, F>
{
    using type = T;
};

template <bool B, class T, class F>
using conditional_t = typename conditional<B, T, F>;

boolの値がtrueの場合を特殊化していますが,デフォルトはtypeがTになって,falseの場合はFになる,と逆にしても問題はありません.

conjunctionおよびconjunction_vの実装


conjunctionは,is_XXX系の型が特定の性質を持っているかを判定するtype traitsに対する論理積になります.通常の論理積と同じく,短絡評価が可能です.

つまり,is_integral_v<float> && is_floating_point_v<float>と書くと,最初のfloatが整数かどうかの判定でfalseになるので,全体としてfalseが決定するので,is_floating_point_v<float>は参照しない,というようなものです.

ただし,ここで問題なのが,&&でtype traitsの結果の論理積を取ってしまうと,参照はしないけれど,テンプレートの実体化は行われている,ということです.

例えば,has_valueというtype traitsがあって,与えられた型TがT::valueというような名前を持つかチェックする場合,has_value<int>が実体化されるとコンパイルエラーになるわけです.しかし,conjunctionを使うと,そういった問題は発生しません.

conjunction<is_floating_point<int>, int>;

最初の時点でfalseになるので,それ以降はテンプレートの実体化が行われず,2つ目のintはint::valueをチェックする,という処理が行われないわけです.

あとは,少しややこしいのが,親となるクラスを決定する方法です.

  • conjunction<>と実引数が0の場合,true_typeを継承.
  • conjunction<T0, T1, ..., Tn>の場合,Ti::valueがfalseなら,Tiを継承
  • conjunction<T0, T1, ..., Tn>の場合,全てのTi::valueがtrueなら,Tn(つまり最後の型)を継承

となるわけです.

というわけで,おそらく次のようになります.


// 実引数が0の場合は,ここで終わる
template <class... B>
struct conjunction : true_type {};

// 実引数が1の場合は,ここで終わる
template <class H>
struct conjunction<H> : conditional_t<static_cast<bool>(H::value), H, H>
{};

// それ以外
template <class H, class... T>
struct conjunction<H, T...> : conditional_t<static_cast<bool>(H::value), conjunction<T...>, H>
{};

template <class... B>
inline constexpr bool conjunction_v = conjunction<B...>::value;

最初の定義は,実引数が0の場合に適用されます.1つの場合,2つ目の定義で::valueを持っているかチェックした上でその型を利用します.
trueでもfalseでも,最後の1つならその型を使うことに変わりはないので,これで問題ないはずです.

3つ目の定義は,2つ以上ある場合で,trueなら残りをチェックし,falseならその型を継承するようにしています.

名前空間myに定義していたとして,次のようなテストをしています.

my::conjunction<my::is_integral<int>> c0;
my::is_integral<int> * pc0 = &c0;

my::conjunction<is_integral<int>, my::is_floating_point<int>> c1;
my::is_floating_point<int> * pc1 = &c1;

my::conjunction<is_integral<int>, my::is_floating_point<int>, int> c2;
my::is_floating_point<int> * pc2 = &c2;

// my::conjunction_v<int>; // コンパイルエラー(int::valueができないため)

static_assert(my::conjunction_v<>);
static_assert(my::conjunction_v<std::is_integral<int>>);
static_assert(my::conjunction_v<std::is_integral<int>, std::is_floating_point<float>>);
static_assert(!my::conjunction_v<std::is_integral<int>, std::is_floating_point<int>>);
// 短絡評価されるので,intを指定していても問題無し
static_assert(!my::conjunction_v<std::is_integral<float>, int>);

clangの実装でもMSVCの実装でも,condtionalを使っていなかったので,もしかしたら何か抜けがあるのかもしれませんが,今のところ上手く動作していそうです.

こういうパターンが網羅できないよ,というのがあれば指摘いただけると助かります.

2020年6月23日火曜日

STLのtype_traitsを実装する その1

C++の標準ライブラリにはtype_traitsという,型に対して様々な判定をしたり,変換をしたりするテンプレートが用意されています.こういうテンプレートをtype traitsと言います.

これを独自に実装するというのは車輪の再発明になりますが,例えばテンプレートで配列の場合のみ特殊化したい場合,どう書けば良いのか,などテンプレートの書き方について色々と勉強になります.

なお,ここではC++17以降を想定しています.

integral_constantの実装

まず,integral_constantを実装します.これは,type_traitsにある色々な判定系のテンプレートの土台になっています.また,boolの場合に限定したbool_constantエイリアステンプレートや,その値がtrueの場合のtrue_type,false_typeは,型が特定の条件を満たすか判定する際の土台として使います.

template <class T, T v>
struct integral_constant {
    static constexpr T value = v;
    using value_type = T;
    using type = integral_constant<T, v>
    constexpr operator value_type() const noexcept { return value; }
    constexpr value_type operator()() const noexcept { return value; }
};
template <bool B>
using bool_constant = integral_constant<bool, B>;
using true_type = bool_constant<true>;
using false_type = bool_constant<false>;

is_sameおよびis_same_vの実装


次に,2つのテンプレートパラメータの型が同じ場合にtrue,違う場合にfalseとなるvalueを持ったis_sameという型を実装します.また,通常はis_same<X, Y>::valueと書いて値を読み取るところを,C++14以降では,直接読み取れるようにした
is_same_vも追加されているので,そちらも実装します.

template <class T, class U>
struct is_same : false_type {};

template <class T>
struct is_same : true_type {};

template <class T, class U>
inline constexpr bool is_same_v = is_same<T, >::value;

このように,通常はfalse_typeを継承していて,型が同じ場合を特殊化してtrue_typeを継承するようにすることで,valueの値は型が同じときだけtrueになります.

これを実装しておくと,次に実装する,型を変形するtype traitsのテストがしやすくなります.is_sameも,次のようにstatic_assertを利用してテストしておきましょう.

static_assert(is_same_v<int, int>);
static_assert(!is_same_v<int, const int>);

本当は,もっと色々なパターンをテストしておいた方が良いでしょうが,その辺は実装する際に色々考えてみると良いでしょう.

remove/add系の実装


次に,型を変形するtype traitsを実装します.何故かというと,その型が何なのかを判定するis_XXX系のtype_traitsではconstなどは無視するケースも多々あり,そこでこれから実装するremove_constなどを利用するからです.

これらの型を変形するtype traitsには,typeという名前で変形後の型にアクセスできます.

まずは,constを取り除くremove_constを実装しましょう.また,typeに直接アクセスできるremove_const_tも実装します.

template <class T>
struct remove_const
{
    using type = T;
};

template <class T>
struct remove_const
{
    using type = T;
};

template <class T>
using remove_const_t = typename remove_const<T>::type;

エイリアステンプレートでtypeと取り出す際には,typenameが必要です.

逆に,constを付けるadd_constの実装はシンプルです.

template <class T>
struct add_const
{
    using type = const T;
};

template <class T>
using add_const_t = typename add_const<T>::type;

constで無い型にはconstが付き,既にconstな型なら追加のconstは無視されるんですね.

では,テストを書いておきましょう.

static_assert(is_same_v<remove_const<int>, int>);
static_assert(is_same_v<remove_const<const int>, int>);
static_assert(is_same_v<remove_const<const int *>, const int *>);
static_assert(is_same_v<remove_const<const int * const>, const int *>);

本当は,もう少し色々書くべきですが,省略します.ここで注目すべきは,ポインタの場合,constはポインタに対して付いているものが取り除かれる,ということです.
const int *がint *になると思った方もいるかもしれませんが,そうはなりません.

こんな感じで,独自のtype_traitsを実装していきます.

remove_constとadd_constのconstをvolatileに置き換えるだけで,remove_volatileとadd_volatileが作れます.

この2つを組み合わせれば,remove_cvやadd_cvが作れます.

その2は……気が向いたら書きます.