以下 (敬称略) に対する返信です。
追記: 更にこの流れで以下のような記事 (敬称略) も登場しています!
詳細は上の @yumetodo さんの 記事を御覧くださいな。ここでは親記事に自分がコメントしたコードの説明についてだけ、できるだけ簡単に載せるにとどめます (と思ったけれど寄り道が多いせいで長くなってしまいました)。
テーマ: enum class 型にビット演算子を追加する簡潔な手法
テーマというか目的というか?は上記の記事と同様です…と書こうと思って思ったのですけれど、誰も端的に書いていないですね。という訳で上の見出しの通りにビット演算子を追加する方法です。背景はいろいろあるけれども、結局はそれだけです。
面倒なので御託はここまでにしてさっさと本体に行きます。
コード
以下、親記事に解説なしでコメントしたコードです。親記事にこれ以上迷惑がかからないように (@prickle さんすみません…) ここで解説・補足します。
先ずはこんな感じのヘッダを作って準備します。
#ifndef enum_utils_provides_bitwise_operators_hpp
#define enum_utils_provides_bitwise_operators_hpp
#include <type_traits>
namespace enum_utils {
template<typename Enum, typename T = Enum>
struct provides_bitwise_operators: std::false_type {};
namespace provides_bitwise_operators_detail {
enum class fallback_enum {};
template<typename T> using safe_underlying_t = typename std::conditional<
!std::is_enum<T>::value, T,
typename std::underlying_type<
typename std::conditional<std::is_enum<T>::value, T, fallback_enum>::type>::type>::type;
template<typename T1, typename T2, bool ForceLHS = false>
using result_t = typename std::enable_if<
(provides_bitwise_operators<T1, T2>::value || provides_bitwise_operators<T2, T1>::value) &&
std::is_same<safe_underlying_t<T1>, safe_underlying_t<T2>>::value,
typename std::conditional<ForceLHS || std::is_enum<T1>::value, T1, T2>::type>::type;
template<typename T>
constexpr safe_underlying_t<T> peel(T flags) {return static_cast<safe_underlying_t<T>>(flags);}
template<typename T>
constexpr result_t<T, T> operator~(T flags) {return static_cast<T>(~peel(flags));}
template<typename T1, typename T2>
constexpr result_t<T1, T2> operator|(T1 flags1, T2 flags2) {
return result_t<T1, T2>(peel(flags1) | peel(flags2));
}
template<typename T1, typename T2>
constexpr result_t<T1, T2> operator&(T1 flags1, T2 flags2) {
return result_t<T1, T2>(peel(flags1) & peel(flags2));
}
template<typename T1, typename T2>
constexpr result_t<T1, T2> operator^(T1 flags1, T2 flags2) {
return result_t<T1, T2>(peel(flags1) ^ peel(flags2));
}
template<typename T1, typename T2>
constexpr result_t<T1, T2, true>& operator|=(T1& flags1, T2 flags2) {
return flags1 = static_cast<T1>(flags1 | flags2);
}
template<typename T1, typename T2>
constexpr result_t<T1, T2, true>& operator&=(T1& flags1, T2 flags2) {
return flags1 = static_cast<T1>(flags1 & flags2);
}
template<typename T1, typename T2>
constexpr result_t<T1, T2, true>& operator^=(T1& flags1, T2 flags2) {
return flags1 = static_cast<T1>(flags1 ^ flags2);
}
}
}
using enum_utils::provides_bitwise_operators_detail::operator~;
using enum_utils::provides_bitwise_operators_detail::operator|;
using enum_utils::provides_bitwise_operators_detail::operator&;
using enum_utils::provides_bitwise_operators_detail::operator^;
using enum_utils::provides_bitwise_operators_detail::operator|=;
using enum_utils::provides_bitwise_operators_detail::operator&=;
using enum_utils::provides_bitwise_operators_detail::operator^=;
#endif
使うときはこんな感じにします。
#include "provides_bitwise_operators.hpp"
// (1) 対象の enum class たち
enum class FooFlags : int { A = 1, B = A << 1, C = B << 1 };
enum class FooFlagsEx : int { D = 0x100 };
// (2) 演算子を追加する
namespace enum_utils {
// これだけで ~ | ^ & |= ^= &= が使える様に。
template<> struct provides_bitwise_operators<FooFlags>: std::true_type {};
// // underlying_type との演算も許す場合は以下も追加:
// template<> struct provides_bitwise_operators<FooFlags, std::underlying_type<FooFlags>::type>:
// std::true_type {};
// // 他の enum class FooFlagsEx との演算も許す場合は以下も追加:
// template<> struct provides_bitwise_operators<FooFlags, FooFlagsEx>: std::true_type {};
}
// (3) デモ
void bar(FooFlags) {}
int main() {
constexpr FooFlags param = FooFlags::A | FooFlags::B;
bar(FooFlags::B);
bar(FooFlags::A | FooFlags::B | FooFlags::C);
FooFlags a = FooFlags::A;
a |= FooFlags::B; // set a bit
a &= ~FooFlags::B; // reset a bit
a ^= FooFlags::B; // toggle a bit
return 0;
}
上記のコードを読んで分かる人は、以降は読まなくて問題ありません。
解説
それで、何から説明しましょうか。。上記コードを眺めてみると、複数の要素が組み合わさっていますね…。
昼休みにぼーっと書いて頂いたコードを読み解いて感心していました。
何とか意味は分かりましたが、自分出かける気がしません。
頭の使い方を変えないと駄目だなと痛感します。
うーん。結局はパターン (idioms) なんです。ゼロから自分で思いついた訳じゃないです。上記の例だと traits だとか SFINAE だとか、あと detail namespace もパターンという程のものではないかもしれませんがよく使われる手法で、これは頭の使い方を変えてぱっと発明したものじゃないです。それらを組み合わせているので入り組んで見えますが、一つ一つの要素に着目すればスタンダードなやり方です。
読者の方がどれだけこれらに達者なのかは分かりませんので全部説明します。簡単に…。
1. Traits/Policy/Concept
Traits, Policy, ..., etc. 1 というのは型による設定を行う様々な方法論の名称です。Traits, Policy, Concept など諸々の概念は分かりにくいので別の記事にしました:
特にここで説明するのは Traits を Concept として使う場合に当たります。現行 C++ で Traits を実現するといえば、もう一択でテンプレートの特殊化 (specialization) を使います。
template<typename T>
struct nakigoe {
static constexpr const char* get_nakigoe() {return "<unknown>";}
};
template<typename T>
void naku(T const& value) {
std::cout << value << " says " << nakigoe<T>::get_nakigoe() << std::endl;
}
// int 特殊化: int 型はわんわん
template<>
struct nakigoe<int> {
static constexpr const char* get_nakigoe() {return "Wan Wan";}
};
// double 特殊化: double はにゃあにゃあ
template<>
struct nakigoe<double> {
static constexpr const char* get_nakigoe() {return "Nyaa Nyaa";}
};
こうです。特殊化を使えば型ごとに設定ができるのが一目瞭然です。特に、特殊化の何が良いって 後から幾らでも自由に特殊化を追加し放題 ということです。カプセル化などの観点から、自由に追加し放題というのは言語としてどうなんだという気もしますが、C++ が強力である所以と表裏一体なので捨てられません。使い方さえ間違えなければ大丈夫です…。たとえば、なんか、新しい型を "がおー" と鳴かせたければ以下を追加すればいいんです。
template<>
struct nakigoe<MySpecialType> {
static constexpr const char* get_nakigoe() {return "Gao Gao Gaoh";}
};
void zoo() {
naku(MySpecialType());
}
元コードの例で行くと、このテンプレート:
template<typename Enum, typename T = Enum>
struct provides_bitwise_operators: std::false_type {};
がそれです。provides_bitwise_operators
が nakigoe
で、value
が get_nakigoe
です。
- 但し、一つの型に対する設定ではなくて
(Enum, T)
という2つの型の組についての設定です。 - また、
value
を手で宣言するのは面倒なので、std::true_type::value
/std::false_type::value
を継承することにします。
2. SFINAE
2.1 SFINAE
これはどこかその辺に説明しているページが沢山ありそうです。本当は厳密なルールは滅茶苦茶面倒くさい (規格 14.8.2-14.8.3 で今数えたら 18 ページもある。読む気しない) し、下手なことを書くと袋叩きに合いそうですが、簡単にいうと関数テンプレートの多重定義解決 (overload resolution) をする時に良い感じに処理してくれる規則です。
例えば、以下のような関数テンプレートの多重定義があると、配列型については1つ目の関数が呼び出され、その他の型については2つめの関数が呼ばれます。
template<typename T, int I> void hello(T (&data)[I]);
template<typename T> void hello(T& data);
これを敷衍して、例えば、以下のような関数テンプレートの多重定義を用意しておけば、nested type として value_type
を持つ型は1つ目の hello
を、param_type
を持つ型は2つめの hello
を呼び出す様になっています。
template<typename T> void hello(T& data, typename T::value_type value);
template<typename T> void hello(T& data, typename T::param_type param);
void example() {
std::vector<int> a;
hello(a, 1); // 1つ目。std::vector<int>::value_type があるので。
}
これらは皆 14.8.3/1 "関数テンプレートの deduction failure (当て嵌め失敗) が起こってもコンパイルエラーにはならず、単に多重定義の候補のリストから抜ける" という、いわゆる SFINAE (substitution failure is not an error) の規則によって実現されます2。
2.2 std::enable_if
で、上記の例2.2の T::value_type
の T の部分を改造して、或る条件を満たしていれば value_type
があるし、条件を満たしていなければ value_type
がないという型を考えることができます。それが std::enable_if
です (実際は value_type
ではなく type
ですが)。
template<typename T>
void hello(
T& data,
typename std::enable_if<"Tに関する好きな条件を指定し放題", int>::type value
);
SFINAE を利用すれば好きな条件で関数を有効にしたり無効にしたりできるということになります。ところで、この "SFINAE で候補から外す" という記述をできる箇所が3箇所あります。
- テンプレート引数 (引数の型およびデフォルト引数) (C++11 以降)
- 戻り値の型
- 引数の型
上記の 1. はどんな関数でも一様に使える方法なので、現在ではこれを専ら使うのが多いでしょう。特に、@yumetodo さんの引用されている std::enable_if<~, std::nullptr_t> をもう定型パターンとして使うのが良いです。"2. 戻り値の型" は引数の型によって戻り値の型も異なるという場合に便利です。C++11 以降では decltype
と組み合わせれば快適です。"3." は前 C++11 時代に使われたもので、今は使い所はないような気がします (考えたことなかったけれど実はあったりするのかしらん)。また 2./3. にはそれぞれ制限があります3。
@yumetodo さんの例は主に "1." を使っていて、"provides_bitwise_operators.hpp" の例では "2." を使っています。見た目が違って見えるかもしれませんが、どちらも同じ SFINAE です。
3. その他いろいろ
3.1 detail namespace
これは簡単です。
-
単に、名前空間 detail の中に他で使わない雑多な関数を集めておくということと思って良いです。
-
後は、機能毎の細かい名前空間に区分けしておくことで ADL に足を撃たれないようにするという効果があります。他と被りそうな名前の非メンバー関数を定義するときには、本当にその機能専用の
namespace hoge_detail
を作ります。
3.2 fallback_enum
typename std::underlying_type<
typename std::conditional<std::is_enum<T>::value, T, fallback_enum>::type>::type>::type
これは規格が std::underlying_type<T>
の T
には enum types しか指定しては駄目という風に言っているのでその対策に、enum でない場合はダミーの fallback_enum を渡すようにしているというものです。fallback_enum は実際は使いません。(※例えば std::underlying_type<NonEnumType>
の実体化に失敗するとそれは SFINAE の範疇ではないので本当にコンパイルエラーになってしまいます。)
本当は場合分けして定義しても良かったのですが、可読性を犠牲にして横着しました…すみません。あるいは @yumetodo さんの記事の underlying_cast
の様に先にテンプレート引数で deduction failure を起こして、underlying_type_t
の評価に到達しないように防波堤を作っておくという風にもできます。
3.3 peel
これは @yumetodo さんの記事の underlying_cast
と同様に、static_cast<なんたら>
を書くのが面倒なので作った、怠け者用の関数です。peel
というのは剥くという意味で、なんとなく思い浮かんだので使いました (ところでナマケモノはバナナの皮とかむくんですかね)。引数に応じてキャスト先を自動で決定するので、キャスト先指定のミスを防げるという効果もあります。std::move
と同じです。
3.4 ^
と ^=
~
はどう考えても必須ですよね。これがないとビットクリアができません。一方で ^
とか ^=
を使うのは変ですかね。自分はフラグの演算でも結構使ってしまいます。例えばビットを toggle するのに使ったり、bitwise !=
として使ったりします。
if ((flags1 & mask) != (flags2 & mask))
の代わりに
if ((flags1 ^ flags2) & mask)
とか書いてしまったり。え、可読性についてですか? す、すみません。。
おわり
もうパーツは説明したので、一番初めに提示した provides_bitwise_operators.hpp
を読み解くことができるのではないかと思います。御覧ください。百聞は一見にしかずです。
余談
-
余談: trait と policy は違うものらしいですが、自分には何処に境界があるのかよく分かりません…。人によって言っていることが違う気がします。例えば、型ごとに静的な値を設定するのが trait で動作を設定するのが policy なのだとか、あるいは、先に目的があってその目的のための設定が policy で、先に型自身に固有の性質があってそれを公開しているのが trait だとか。でも、両者の線引きって微妙です。だって公開するからには絶対目的があるはずで、利用価値もないのにたとえば
<type_traits>
なんていうヘッダがどこぞから湧いて出てきたりしないとは思いませんか。更に最近では (というよりなかなか収束せず最近でも) concept だとか言っていて、たぶん concept はある種の traits 実現のための専用文法といえるんだろうと思いますが、これらの抽象概念の関係について考えるとよく分からなくなるので考えない事にしましょう (誰か快刀乱麻な説明を持っていたら教えてください)。 ↩ -
例2.1
T(&)[N]
vsT&
は 14.8.2.4 & 14.8.2.5 の partial ordering による比較によって片方が deduction failure になって候補から外れる (らしい) です (よく理解していない。規格を読んで理解しようと思ったが眠くなったので諦める。参考: 本の虫: Partial Orderingの理解)。次の、typename T::value_type vs typename T::param_type
の例に関しては 14.8.2/8.4 で deduction failure が起こって候補から外れます。 ↩ -
制限: "2. 戻り値の型" による SFINAE は戻り値の型がないと使えませんので、コンストラクタや変換演算子などで使えません。また実引数に対する推論を引き金に SFINAE しているので無引数関数にも使えません。"3. 引数の型" による SFINAE は通常ダミー引数 (デフォルト引数とペアで指定する使われない引数) を以て行いますが、これは引数の数が予め決まっている演算子の多重定義で使えません。 ↩