40
34

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

君の名は・・・enum class

Last updated at Posted at 2016-10-20

https://qiita-image-store.s3.amazonaws.com/0/94177/bad14252-5d04-b289-a3c3-8de401452c91.png
https://qiita-image-store.s3.amazonaws.com/0/94177/d0cf1077-71ed-51ed-b8b0-c114eb6afe90.png
https://qiita-image-store.s3.amazonaws.com/0/94177/af75d6ef-1c6d-70f8-1d00-e77932af499d.png
https://qiita-image-store.s3.amazonaws.com/0/94177/64174025-1492-17f2-9fa2-15087830ed08.png

はい、雑コラすみません。きっと
君の名は・・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定数を作ってきました。

VS2013のios_baseの実装
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の実装戦略としては

  1. 引数を基底型にstatic_castする
  2. 演算する
  3. もとの型に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_operatorshas_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

40
34
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
40
34

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?