[注意] 今から初めてこの記事を読む人へ
本文はサラッと読み流して、コメントを参照して下さい。
@yumetodoさんと@akinomyogaさんがコメントでやり取りされている内容が簡潔で汎用性も高い実装方法だと思います。
@yumetodoさん、@akinomyogaさん素晴らしい解決策をありがとうございました。
と、同時に私が理解できていないため、本文に反映できずにすみません。
(2016/10/21追記)
反映できない私の代わりに @yumetodoさん、@akinomyogaさんが記事を書いてくれました!
C++すげーって感動できます。こちらは読み飛ばして、以下の記事に飛んで下さいませ。
君の名は・・・enum class
Re2: C++のscoped enumで関数のフラグ指定をしたい & 君の名は・・・enum class
関数でOR使って指定することありますよね
C言語でライブラリを使っていて、APIの動作を定数のORで指定することがあります。
古き良きC言語のファイル書き込みであれば、次のような感じで。
int fd = open("foo.txt", O_WRONLY | O_APPEND);
このやり方で私がハマってしまう点
まあ、私がヘボいだけなのですが、OpenGL触ってるとココらへんでめちゃくちゃバグを出しまくるわけです。
違う定数を渡しちゃったり、違う種類の定数をORしちゃったり。
そして想定しない動作に悩むと・・・。
scoped enumで部分的に解決できた
やっとこれを解消できるかと思ったのがC++11で導入されたscoped enumでした。
scoped enumを引数型にすればコンパイラが叱ってくれて悩まずに済むのが凄い快適。
しかしこれOR演算とかする用途は想定していないので、いまいち使い勝手が良くない。
enum class Foo {
A = 1,
B = 1 << 1,
C = 1 << 2,
};
void func(Foo foo) {
(void)foo;
}
int main() {
func(0); // -> NG
// no matching function for call to 'func
func(Foo::A); // -> OK
func(Foo::A | Foo::B); // -> NG
// invalid operands to binary expression ('Foo' and 'Foo')
return 0;
}
Vulkan C+ APIはどうやって解決してるのか?
Open-Source Vulkan C++ APIを調べてたら、これに対する技術解を見つけました。
Improvements to Bit Flagsに色々書いてあります。とても丁寧なドキュメントに感謝。
具体的な実装はvulkan.hppの先頭のクラスFlags
を見てください。
template <typename BitType, typename MaskType = VkFlags>
class Flags
{
public:
Flags()
: m_mask(0)
{
}
Flags(BitType bit)
: m_mask(static_cast<MaskType>(bit))
{
}
Flags(Flags<BitType> const& rhs)
: m_mask(rhs.m_mask)
{
}
Flags<BitType> & operator=(Flags<BitType> const& rhs)
{
m_mask = rhs.m_mask;
return *this;
}
Flags<BitType> & operator|=(Flags<BitType> const& rhs)
{
m_mask |= rhs.m_mask;
return *this;
}
// 中略
private:
MaskType m_mask;
};
ちょっと長いのですが、やっていることは機械的なのですぐわかるかと。
BitType
の方にscoped enumを指定して、MaskTypeの方にscoped enumの基底型を指定して使います。
#include "vulkan.hpp"
enum class Foo : VkFlags {
a = 1,
b = 2,
c = 3,
};
using FooFlags = vk::Flags<Foo>;
// このオペレータは個別に定義が必要。
inline FooFlags operator|(Foo bit0, Foo bit1) {
return FooFlags(bit0) | bit1;
}
int main(int argc, char const* argv[]) {
FooFlags flags;
flags = Foo::a | Foo::b; // -> OK
flags |= Foo::a; // -> OK
flags = 0; // -> NG
return 0;
}
蛇足ですが、自前の簡易版を作りました
個人的に関数のフラグ指定時に!
とか^
を使うことがないので、|
だけに対応した別バージョンを作ってみました。
私はもうC++14でしか書かないのでunderlying_typeを使って基底型の指定を不要にしています。
(2016/10/04修正)
yumetodoさんに、コメントしてもらいました。
underlying_typeはC++11からありました。正しい情報ありがとうございます。
何か問題ありそうですが、とりあえず問題なく使えてます。
#詳しい方、なんか問題あるようでしたら教えてください。
#include <type_traits>
template <typename T>
class Param {
public:
typedef typename std::underlying_type<T>::type type;
type value;
constexpr Param() : value() {}
constexpr Param(const Param<T>& rhs) : value(rhs.value) {}
constexpr Param(T flag) : value(static_cast<type>(flag)) {}
constexpr Param<T>& operator|=(Param<T> rhs) {
typedef typename Param<T>::type type;
value |= static_cast<type>(rhs.value);
return *this;
}
};
template <typename T>
constexpr Param<T> operator|(Param<T> lhs, Param<T> rhs) {
return Param<T>(lhs.value | rhs.value);
}
template <typename T>
constexpr Param<T> operator|(T lhs, T rhs) {
typedef typename Param<T>::type type;
return static_cast<T>(static_cast<type>(lhs) | static_cast<type>(rhs));
}
// === 以下サンプル ===
enum class FooFlag : int { A = 1, B = 1 << 1 };
void bar(Param<FooFlag> param = {}) {
(void)param;
return;
}
int main(void) {
constexpr Param<FooFlag> param = FooFlag::A | FooFlag::B;
static_assert(static_cast<int>(param.value) == 3, "foobar");
bar(FooFlag::B);
bar(FooFlag::A | FooFlag::B);
bar();
bar({}); // これだけいまいち・・・
return 0;
}
(2016/10/13 修正)
yumetodoさんにコメントしてもらったconstexpr化を試してみました。
見よう見まねなのですが、一応できている・・・はず。
まとめ
- scoped enumを使って、関数にフラグ指定する時の凡ミスが減らせるので、積極的に使いたい
- OR指定する場合には素のscoped enumでは対応できないのでちょっとした工夫が必要。
- 単純なクラスはconstexpr化するのを忘れないように。