はい、雑コラすみません。きっと
君の名は・・YARN!
なんてものを見たせいです(責任転嫁)。
まだ映画上映していますから見に行きましょう。原作小説も絶賛発売中なので買いましょう(宣伝は基本)。
Re:C++のscoped enumで関数のフラグ指定をしたい
改めましてみなさま、ナマステ。この記事は
C++のscoped enumで関数のフラグ指定をしたい
への返信記事です。
同時に
Re2: C++のscoped enumで関数のフラグ指定をしたい & 君の名は・・・enum class
から返信されています。
見てわかるように、「君の名は。」と「Re:ゼロから始める異世界生活」とC++の加重平均をとったような記事です。
なんか自分で作りたくなる人がいるようで2つほどこの記事が参照されています(が圧倒的akinomyoga氏の記事の影響力。そんなに異なるenum class同士の演算を定義したいか?)
注意
この記事はSFINAEについてそこはかとなく理解していることが求められます。これを満たさない人は先に各自ググってから読み進んでください。
そもそもフラグ定数とは
例えばWin32APIでレジストリの値を読み取るとき、
HKEY key
const TCHAR* sub_key_root = _T("Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders")
if (ERROR_SUCCESS != RegOpenKeyEx(HKEY_CURRENT_USER, sub_key_root, 0, KEY_READ | KEY_WOW64_64KEY, &key))
throw std::runtime_error("error");
DWORD dwType = REG_SZ;
DWORD dwByte = 32;
if (ERROR_SUCCESS != RegQueryValueEx(key, _T("Personal"), 0, &dwType, nullptr, &dwByte) || REG_SZ != dwType)
throw std::runtime_error("error");
std::basic_string<TCHAR> buf;
buf.resize(dwByte);
RegQueryValueEx(key, _T("Personal"), 0, nullptr, (LPBYTE)&buf[0], &dwByte);
buf.resize(std::char_traits<TCAHR>::length(buf.c_str()));
KEY_READ | KEY_WOW64
みたいに指定するわけですが、これがいわゆるフラグ定数ですね。
C++なのにマクロをつかうの?
というわけで、これまでC++ではフラグ用のクラスを作ってそこにstatic const定数を作ってきました。
template<class _Dummy>
class _Iosb
{ // define templatized bitmask/enumerated types, instantiate on demand
public:
enum _Iostate
{ // constants for stream states
_Statmask = 0x17};
static const _Iostate goodbit = (_Iostate)0x0;
static const _Iostate eofbit = (_Iostate)0x1;
static const _Iostate failbit = (_Iostate)0x2;
static const _Iostate badbit = (_Iostate)0x4;
static const _Iostate _Hardfail = (_Iostate)0x10;
};
#define _BITMASK(Enum, Ty) typedef int Ty
class _CRTIMP2_PURE ios_base
: public _Iosb<int>
{ // base class for ios
public:
_BITMASK(_Iostate, iostate);
}:
ちなみにC++11ではstatic constexpr
にするようになっています。
std::ios_base::iostate - cppreference.com
static const式の問題
フラグの型がintとかの整数型になってしまうのでフラグ同士混ぜられる
struct Flag1{
enum type {};
static constexpr type a = (type)1;
static constexpr type b = (type)2;
};
struct Flag2{
enum type {};
static constexpr type a = (type)4;
static constexpr type b = (type)8;
};
void f(int){}
void g(Flag1::type){}
int main()
{
f(Flag1::a);//OK
f(Flag2::b);//OKおおっと!?
f(Flag1::a | Flag1::b);//OK
f(Flag1::a | Flag2::b);//OKおおっと!?
g(Flag1::a);//OK
//g(Flag2::b);//NG:no matching function for call to 'g'
//g(Flag1::a | Flag1::b);//NGおおっと!?:no known conversion from 'int' to 'Flag1::type' for 1st argument
//g(Flag1::a | Flag2::b);//NG:no known conversion from 'int' to 'Flag1::type' for 1st argument
}
C++11にはenum classがある
そこでenum class
の出番です。
enum class Flag1 : int {
a = 1,
b = 2
};
enum class Flag2 : int {
a = 4,
b = 8
};
void f(int){}
void g(Flag1){}
int main()
{
//f(Flag1::a);//NG:no known conversion from 'Flag1' to 'int' for 1st argument
//f(Flag2::b);//NG:no known conversion from 'Flag2' to 'int' for 1st argument
//f(Flag1::a | Flag1::b);//NG:invalid operands to binary expression ('Flag1' and 'Flag1')
//f(Flag1::a | Flag2::b);//NG:invalid operands to binary expression ('Flag1' and 'Flag2')
g(Flag1::a);//OK
//g(Flag2::b);//NG:no known conversion from 'Flag2' to 'Flag1' for 1st argument
//g(Flag1::a | Flag1::b);//NGおおっと!?:invalid operands to binary expression ('Flag1' and 'Flag1')
//g(Flag1::a | Flag2::b);//NG:invalid operands to binary expression ('Flag1' and 'Flag2')
}
enum class
は基底型に暗黙変換できませんし、同じ基底型のべつのenum class
からの変換ももちろんできません。
しかしこれでは同じenum class
同士のOR演算もできません。
operator overloadしよう
enum class Flag1 : int {
a = 1,
b = 2
};
enum class Flag2 : int {
a = 4,
b = 8
};
constexpr Flag1 operator|(Flag1 l, Flag1 r){
return static_cast<Flag1>(static_cast<int>(l) | static_cast<int>(r));
}
void f(int){}
void g(Flag1){}
int main()
{
//f(Flag1::a);//NG:no known conversion from 'Flag1' to 'int' for 1st argument
//f(Flag2::b);//NG:no known conversion from 'Flag2' to 'int' for 1st argument
//f(Flag1::a | Flag1::b);//NG:no known conversion from 'Flag1' to 'int' for 1st argument
//f(Flag1::a | Flag2::b);//NG:invalid operands to binary expression ('Flag1' and 'Flag2')
g(Flag1::a);//OK
//g(Flag2::b);//NG:no known conversion from 'Flag2' to 'Flag1' for 1st argument
g(Flag1::a | Flag1::b);//OK
//g(Flag1::a | Flag2::b);//NG:invalid operands to binary expression ('Flag1' and 'Flag2')
}
機械的にoperator overloadを書くなんてあなた、怠惰ですね~。
この方法の問題点は、各enum class
ごとにoperator overloadしないといけない点です。
しかもOR演算だけでなくて、& &= |=
ぐらい、場合によっては~ ^ ^=
も使えてほしいと思うので、7*[enum classの個数]
分のoperator overloadを書く羽目に・・・。
絶対イヤだ。
そこでconcept的なoperator overloadですよ
まずはconceptもどきをでっち上げる
これが肝です。それぞれのenum class
が持つ性質というかインターフェースというかこの場合もっと言ってしまえば「どんなoperatorが使えるのか」を宣言するものと言う意味で私はconceptと言っています。
#include <type_traits>
namespace enum_concept{
template<typename T>
struct has_bitwise_operators : std::false_type {};
template<typename T>
struct has_and_or_operators : has_bitwise_operators<T> {};
}
has_and_or_operators
がhas_bitwise_operators
を継承しているのは、AND/OR演算がその他Bit演算に含まれるので、has_bitwise_operators
を満たすenum class
は当然has_and_or_operators
を満たしますね。
いつものあれを書く
いつものあれ、で通じる人どのくらいいるんだろうか。
std::enable_ifを使ってオーバーロードする時、enablerを使う?
これです。次の項でSFINAEするのに使います
namespace type_traits{
template<bool con> using concept_t = typename std::enable_if<con, std::nullptr_t>::type;
template<typename T> using underlying_type_t = typename std::underlying_type<T>::type;//C++11にはない
}
SFINAEしつつoperator overloadを書く
あんま解説することもないけど一応。
まず、enum
にしろenum class
にしろ、基底型というものが存在します。enum class
は基底型に明示変換できます。基底型は整数型なので、これからorverload しようとしているoperatorはすでに持っています。基底型はstd::underlying_type
で取得できます
よって各operator overloadの実装戦略としては
- 引数を基底型に
static_cast
する - 演算する
- もとの型に
static_cast
する
と言うものになります。
ただstatic_cast<underlying_type_t<T>>
と書くのはだるい&可読性下がる&typoしやすくなると、3拍子揃ってやるべきではないので、underlying_cast
というのをでっち上げています。積極的に怠けていこうな!。まあstd::underlying_type
のtemplate第一引数にenumじゃない型を渡せないからSFINAEしておきたい、という話もありますが。C++がどんな問題でももう一段階のラッパーをかますことで解決できる。ただし、ラッパーが多すぎるという問題を除いては。という言語だとよくわかりますね。
namespace detail{
using namespace type_traits;
template<typename T, concept_t<std::is_enum<T>::value> = nullptr>
constexpr underlying_type_t<T> underlying_cast(T e) { return static_cast<underlying_type_t<T>>(e); }
}
template<typename T, type_traits::concept_t<enum_concept::has_and_or_operators<T>::value> = nullptr>
constexpr T operator&(T l, T r) {return static_cast<T>(detail::underlying_cast(l) & detail::underlying_cast(r));}
template<typename T, type_traits::concept_t<enum_concept::has_and_or_operators<T>::value> = nullptr>
T& operator&=(T& l, T r) {
l = static_cast<T>(detail::underlying_cast(l) & detail::underlying_cast(r));
return l;
}
template<typename T, type_traits::concept_t<enum_concept::has_and_or_operators<T>::value> = nullptr>
constexpr T operator|(T l, T r) {return static_cast<T>(detail::underlying_cast(l) | detail::underlying_cast(r));}
template<typename T, type_traits::concept_t<enum_concept::has_and_or_operators<T>::value> = nullptr>
T& operator|=(T& l, T r) {
l = static_cast<T>(detail::underlying_cast(l) | detail::underlying_cast(r));
return l;
}
template<typename T, type_traits::concept_t<enum_concept::has_bitwise_operators<T>::value> = nullptr>
constexpr T operator^(T l, T r) {return static_cast<T>(detail::underlying_cast(l) ^ detail::underlying_cast(r));}
template<typename T, type_traits::concept_t<enum_concept::has_bitwise_operators<T>::value> = nullptr>
T& operator^=(T& l, T r) {
l = static_cast<T>(detail::underlying_cast(l) ^ detail::underlying_cast(r));
return l;
}
template<typename T, type_traits::concept_t<enum_concept::has_bitwise_operators<T>::value> = nullptr>
constexpr T operator~(T op) {return static_cast<T>(~detail::underlying_cast(op));}
対象となるenum classを定義
enum class Flag1 : int {
a = 1,
b = 2
};
enum class Flag2 : int {
a = 4,
b = 8
};
conceptもどきのクラスのtemplate特殊化を書く
今回はFlag1
は& &= | |= ^ ^= ~
、Flag2
は& &= | |=
の演算ができるようにしてみます。
namespace enum_concept{
template<> struct has_bitwise_operators<Flag1> : std::true_type {};
template<> struct has_and_or_operators<Flag2> : std::true_type {};
}
さきほど、has_bitwise_operators
やhas_and_or_operators
はoperator overloadのSFINAEの条件に使っていました。ここで許可するtemplate特殊化を書くことでoperator overloadが有効になりますね。元記事の @akinomyoga さんのコメントに書かれたコードではunderlying_type との演算や他のenum classとの演算を有効にする手段も提供していますが、一体どうやったらそんなものが必要なのか理解できないので(それくらいキャスト書け)今回は省略しています。
使ってみる
void f(Flag1){}
void g(Flag2){}
int main()
{
f(Flag1::a | Flag1::b);
f(Flag1::a & Flag1::b);
f(Flag1::a ^ Flag1::b);
f(~Flag1::a);
g(Flag2::a | Flag2::b);
g(Flag2::a & Flag2::b);
//g(Flag2::a ^ Flag2::b);//invalid operands to binary expression ('Flag2' and 'Flag2')
//g(~Flag2::a);//invalid argument type 'Flag2' to unary expression
}
注意点
今回はC++11の範囲で書きましたが、代入もする演算子はC++14じゃないとconstexprにできません。なおVisual Studio 2013ではconstexprは使えませんが、上のコードからconstexprを置換して取り除けば多分動くと思います。
得られたもの
- フラグ指定する時の凡ミスが減らせるようになった
- enum classに希望が持てた
失ったもの
- この記事を読むのに費やした時間
それにしても
conceptがない世界はいややー!来世はconceptがある世界に産まれさせてくださーい!
C++11のconceptとC++17に提案されていたconceptについては
帰ってきたコンセプト | Boost勉強会 #16 大阪
を見てください。まあC++17にconceptが入らないことが確定したけどな!
余談
凄まじくどうでもいい話ですが、冒頭の雑コラ、最初3枚はシーン的には映画最後のシーンで(三葉と瀧がタイムスリップしてる絵だけど)、2021年12月という設定らしいのでC++17とC++20は出てますね。C++23に向けて標準化委員会にまたconceptとかが提案されていることでしょう。
4枚目は2013年9月ごろのはずなので、C++11にconceptが入らないことになり間もなくC++14がでるけどやっぱりなんでconceptないんや!という時期ですね。C++17に提案されていてまたもrejectを喰らったConcept Liteの作業が始まったのが2014/2/17らしいのでまだ三葉は再提案の話は知らないはずですね。
cf.)
【ネタバレ解説】「君の名は。」読者と共に読解入れ替わり時系列、図解で解説!「転校生」どころじゃなかった…チェ・ブンブンのティーマ - Part 2