如何在 C++ 中使用选项 true、false、default 和 toggle 实现标志?
How to implement flags with the options true,false,default and toggle in C++?
我目前正在尝试想出一种巧妙的方法来实现标志,除了通常的 "true" 和 "false".
标志的一般问题是,一个人有一个函数并想通过传递某些参数来定义它的行为("do something" 或 "don't do something")。
单旗
使用单个(布尔)标志,解决方案很简单:
void foo(...,bool flag){
if(flag){/*do something*/}
}
这里添加默认值特别容易,只需将函数更改为
void foo(...,bool flag=true)
并在没有标志参数的情况下调用它。
多个标志
一旦标志数量增加,我通常看到和使用的解决方案是这样的:
typedef int Flag;
static const Flag Flag1 = 1<<0;
static const Flag Flag2 = 1<<1;
static const Flag Flag3 = 1<<2;
void foo(/*other arguments ,*/ Flag f){
if(f & Flag1){/*do whatever Flag1 indicates*/}
/*check other flags*/
}
//call like this:
foo(/*args ,*/ Flag1 | Flag3)
这样做的好处是,您不需要为每个标志设置一个参数,这意味着用户可以设置他喜欢的标志,而忘记那些他不关心的标志。特别是你不会接到像 foo (/*args*/, true, false, true)
这样的电话,你必须计算哪个 true/false 表示哪个标志。
这里的问题是:
如果您设置了默认参数,一旦用户指定任何标志,它就会被覆盖。不可能像 Flag1=true, Flag2=false, Flag3=default
.
这样思考
显然,如果我们想要有 3 个选项(真、假、默认),我们需要为每个标志传递至少 2 位。因此,虽然它可能不是必需的,但我想任何实现都应该很容易使用第 4 状态来指示切换(=!默认)。
我有两种方法,但我对这两种方法都不满意:
方法 1:定义 2 个标志
到目前为止,我尝试过使用这样的东西:
typedef int Flag;
static const Flag Flag1 = 1<<0;
static const Flag Flag1False= 1<<1;
static const Flag Flag1Toggle = Flag1 | Flag1False;
static const Flag Flag2= 1<<2;
static const Flag Flag2False= 1<<3;
static const Flag Flag2Toggle = Flag2 | Flag2False;
void applyDefault(Flag& f){
//do nothing for flags with default false
//for flags with default true:
f = ( f & Flag1False)? f & ~Flag1 : f | Flag1;
//if the false bit is set, it is either false or toggle, anyway: clear the bit
//if its not set, its either true or default, anyway: set
}
void foo(/*args ,*/ Flag f){
applyDefault(f);
if (f & Flag1) //do whatever Flag1 indicates
}
但是我不喜欢的是,每个标志使用两个不同的位。这导致 "default-true" 和 "default-false" 标志的代码不同,并且导致必要的 if 而不是 applyDefault()
.
中的一些漂亮的按位操作
方法 2:模板
通过这样定义模板-class:
struct Flag{
virtual bool apply(bool prev) const =0;
};
template<bool mTrue, bool mFalse>
struct TFlag: public Flag{
inline bool apply(bool prev) const{
return (!prev&&mTrue)||(prev&&!mFalse);
}
};
TFlag<true,false> fTrue;
TFlag<false,true> fFalse;
TFlag<false,false> fDefault;
TFlag<true,true> fToggle;
我能够将 apply
压缩成一个按位运算,除了 1 个参数之外的所有参数在编译时都是已知的。因此,使用 TFlag::apply
直接编译(使用 gcc)为与 return true;
、return false;
、return prev;
或 return !prev;
相同的机器代码,这是非常有效的,但这意味着如果我想将 TFlag
作为参数传递,我必须使用模板函数。从 Flag
继承并使用 const Flag&
作为参数增加了虚函数调用的开销,但使我免于使用模板。
但是我不知道如何将其扩展到多个标志...
问题
所以问题是:
我如何在 C++ 中的单个参数中实现多个标志,以便用户可以轻松地将它们设置为 "true"、"false" 或 "default"(通过不设置特定标志)或(可选) 表示 "whatever is not default"?
class 是否有两个整数,使用类似的按位运算,如模板方法和自己的按位运算符?如果是这样,有没有办法让编译器可以选择在编译时执行大部分按位运算?
编辑澄清:
我不想将 4 个不同的标志 "true"、"false"、"default"、"toggle" 传递给函数。
例如。想一想在标记用于 "draw border"、"draw center"、"draw fill color"、"blurry border"、"let the circle hop up and down"、"do whatever other fancy stuff you can do with a circle" 的地方绘制的圆圈。 ...
对于那些 "properties" 中的每一个,我想传递一个值为 true、false、default 或 toggle 的标志。
因此函数可能决定默认绘制边框、填充颜色和中心,但 none 其余部分。一个调用,大致是这样的:
draw_circle (DRAW_BORDER | DONT_DRAW_CENTER | TOGGLE_BLURRY_BORDER) //or
draw_circle (BORDER=true, CENTER=false, BLURRY=toggle)
//or whatever nice syntax you come up with....
应该绘制边框(由标志指定),不绘制中心(由标志指定),模糊边框(标志表示:不是默认值)并绘制填充颜色(未指定,但它是默认值) .
如果我后来决定不再默认绘制中心而是默认模糊边框,调用应该绘制边框(由标志指定),而不是绘制中心(由标志指定),not 模糊边框(现在模糊是默认的,但我们不想要默认)并绘制填充颜色(没有标志,但它是默认的)。
如果我理解这个问题,您可以通过使用 bool
的隐式构造函数和默认构造函数创建一个简单的 class 来解决问题:
class T
{
T(bool value):def(false), value(value){} // implicit constructor from bool
T():def(true){}
bool def; // is flag default
bool value; // flag value if flag isn't default
}
并像这样在函数中使用它:
void f(..., T flag = T());
void f(..., true); // call with flag = true
void f(...); // call with flag = default
不是很漂亮,但非常简单(从您的方法 1 构建):
#include <iostream>
using Flag = int;
static const Flag Flag1 = 1<<0;
static const Flag Flag2 = 1<<2;
// add more flags to turn things off, etc.
class Foo
{
bool flag1 = true; // default true
bool flag2 = false; // default false
void applyDefault(Flag& f)
{
if (f & Flag1)
flag1 = true;
if (f & Flag2)
flag2 = true;
// apply off flags
}
public:
void operator()(/*args ,*/ Flag f)
{
applyDefault(f);
if (flag1)
std::cout << "Flag 1 ON\n";
if (flag2)
std::cout << "Flag 2 ON\n";
}
};
void foo(/*args ,*/ Flag flags)
{
Foo f;
f(flags);
}
int main()
{
foo(Flag1); // Flag1 ON
foo(Flag2); // Flag1 ON\nFlag2 ON
foo(Flag1 | Flag2); // Flag1 ON\nFlag2 ON
return 0;
}
您的评论和回答让我找到了一个我喜欢并想与您分享的解决方案:
struct Default_t{} Default;
struct Toggle_t{} Toggle;
struct FlagSet{
uint m_set;
uint m_reset;
constexpr FlagSet operator|(const FlagSet other) const{
return {
~m_reset & other.m_set & ~other.m_reset |
~m_set & other.m_set & other.m_reset |
m_set & ~other.m_set,
m_reset & ~other.m_reset |
~m_set & ~other.m_set & other.m_reset|
~m_reset & other.m_set & other.m_reset};
}
constexpr FlagSet& operator|=(const FlagSet other){
*this = *this|other;
return *this;
}
};
struct Flag{
const uint m_bit;
constexpr FlagSet operator= (bool val) const{
return {(uint)val<<m_bit,(!(uint)val)<<m_bit};
}
constexpr FlagSet operator= (Default_t) const{
return {0u,0u};
}
constexpr FlagSet operator= (Toggle_t) const {
return {1u<<m_bit,1u<<m_bit};
}
constexpr uint operator& (FlagSet i) const{
return i.m_set & (1u<<m_bit);
}
constexpr operator FlagSet() const{
return {1u<<m_bit,0u}; //= set
}
constexpr FlagSet operator|(const Flag other) const{
return (FlagSet)*this|(FlagSet)other;
}
constexpr FlagSet operator|(const FlagSet other) const{
return (FlagSet)*this|other;
}
};
constexpr uint operator& (FlagSet i, Flag f){
return f & i;
}
所以基本上 FlagSet
包含两个整数。一个用于设置,一个用于重置。不同的组合代表该特定位的不同操作:
{false,false} = Default (D)
{true ,false} = Set (S)
{false,true } = Reset (R)
{true ,true } = Toggle (T)
operator|
使用了相当复杂的按位运算,旨在满足
D|D = D
D|R = R|D = R
D|S = S|D = S
D|T = T|D = T
T|T = D
T|R = R|T = S
T|S = S|T = R
S|S = S
R|R = R
S|R = S (*)
R|S = R (*)
(*) 中的非交换行为是由于这样一个事实,即我们不知何故需要能够决定哪个是 "default" 哪个是 "user defined"。所以如果值有冲突,左边的优先。
Flag
class 代表一个标志,基本上是一个位。使用不同的 operator=()
重载可以使某种 "Key-Value-Notation" 直接转换为 FlagSet,其中 m_bit
位置的位对设置为先前定义的对之一。默认情况下 (operator FlagSet()
) 这将转换为给定位上的 Set(S) 操作。
class 还为按位或自动转换为 FlagSet
和 operator&()
提供了一些重载,以实际比较 Flag
和 FlagSet
。在此比较中,Set(S) 和 Toggle(T) 都被视为 true
,而 Reset(R) 和 Default(D) 都被视为 false
.
使用它非常简单并且非常接近 "usual" 标志实现:
constexpr Flag Flag1{0};
constexpr Flag Flag2{1};
constexpr Flag Flag3{2};
constexpr auto NoFlag1 = (Flag1=false); //Just for convenience, not really needed;
void foo(FlagSet f={0,0}){
f |= Flag1|Flag2; //This sets the default. Remember: default right, user left
cout << ((f & Flag1)?"1":"0");
cout << ((f & Flag2)?"2":"0");
cout << ((f & Flag3)?"3":"0");
cout << endl;
}
int main() {
foo();
foo(Flag3);
foo(Flag3|(Flag2=false));
foo(Flag3|NoFlag1);
foo((Flag1=Toggle)|(Flag2=Toggle)|(Flag3=Toggle));
return 0;
}
输出:
120
123
103
023
003
关于效率的最后一句话:虽然我没有在没有所有 constexpr
关键字的情况下对其进行测试,但有了这些代码:
bool test1(){
return Flag1&((Flag1=Toggle)|(Flag2=Toggle)|(Flag3=Toggle));
}
bool test2(){
FlagSet f = Flag1|Flag2 ;
return f & Flag1;
}
bool test3(FlagSet f){
f |= Flag1|Flag2 ;
return f & Flag1;
}
编译为(在 gcc.godbolt.org 上使用 gcc 5.3)
test1():
movl , %eax
ret
test2():
movl , %eax
ret
test3(FlagSet):
movq %rdi, %rax
shrq , %rax
notl %eax
andl , %eax
ret
虽然我并不完全熟悉汇编程序代码,但这看起来像是非常基本的按位运算,并且可能是您在不内联测试函数的情况下可以获得的最快速度。
如果我理解正确,您需要一种简单的方法将一个或多个标志作为单个参数传递给函数,and/or一个对象跟踪一个或多个标志的简单方法单变量,对吗?一种简单的方法是将标志指定为类型化枚举,并使用足够大的无符号基础类型来容纳您需要的所有标志。例如:
/* Assuming C++11 compatibility. If you need to work with an older compiler, you'll have
* to manually insert the body of flag() into each BitFlag's definition, and replace
* FLAG_INVALID's definition with something like:
* FLAG_INVALID = static_cast<flag_t>(-1) -
* (FFalse + FTrue + FDefault + FToggle),
*/
#include <climits>
// For CHAR_BIT.
#include <cstdint>
// For uint8_t.
// Underlying flag type. Change as needed. Should remain unsigned.
typedef uint8_t flag_t;
// Helper functions, to provide cleaner syntax to the enum.
// Not actually necessary, they'll be evaluated at compile time either way.
constexpr flag_t flag(int f) { return 1 << f; }
constexpr flag_t fl_validate(int f) {
return (f ? (1 << f) + fl_validate(f - 1) : 1);
}
constexpr flag_t register_invalids(int f) {
// The static_cast is a type-independent maximum value for unsigned ints. The compiler
// may or may not complain.
// (f - 1) compensates for bits being zero-indexed.
return static_cast<flag_t>(-1) - fl_validate(f - 1);
}
// List of available flags.
enum BitFlag : flag_t {
FFalse = flag(0), // 0001
FTrue = flag(1), // 0010
FDefault = flag(2), // 0100
FToggle = flag(3), // 1000
// ...
// Number of defined flags.
FLAG_COUNT = 4,
// Indicator for invalid flags. Can be used to make sure parameters are valid, or
// simply to mask out any invalid ones.
FLAG_INVALID = register_invalids(FLAG_COUNT),
// Maximum number of available flags.
FLAG_MAX = sizeof(flag_t) * CHAR_BIT
};
// ...
void func(flag_t f);
// ...
class CL {
flag_t flags;
// ...
};
请注意,这假设 FFalse
和 FTrue
应该是不同的标志,两者可以同时指定。如果您希望它们相互排斥,则需要进行一些小改动:
// ...
constexpr flag_t register_invalids(int f) {
// Compensate for 0th and 1st flags using the same bit.
return static_cast<flag_t>(-1) - fl_validate(f - 2);
}
// ...
enum BitFlag : flag_t {
FFalse = 0, // 0000
FTrue = flag(0), // 0001
FDefault = flag(1), // 0010
FToggle = flag(2), // 0100
// ...
或者,您可以修改 flag()
:
而不是修改 enum
本身
// ...
constexpr flag_t flag(int f) {
// Give bit 0 special treatment as "false", shift all other flags down to compensate.
return (f ? 1 << (f - 1) : 0);
}
// ...
constexpr flag_t register_invalids(int f) {
return static_cast<flag_t>(-1) - fl_validate(f - 2);
}
// ...
enum BitFlag : flag_t {
FFalse = flag(0), // 0000
FTrue = flag(1), // 0001
FDefault = flag(2), // 0010
FToggle = flag(3), // 0100
// ...
虽然我认为这是最简单的方法,并且如果您为 flag_t
选择尽可能小的基础类型,则可能是内存效率最高的方法,但它也可能是最没有用的。 [此外,如果您最终使用这个或类似的东西,我建议将辅助函数隐藏在命名空间中,以防止全局命名空间出现不必要的混乱。]
为什么我们不能为此使用枚举?这是我最近使用的解决方案:
// Example program
#include <iostream>
#include <string>
enum class Flag : int8_t
{
F_TRUE = 0x0, // Explicitly defined for readability
F_FALSE = 0x1,
F_DEFAULT = 0x2,
F_TOGGLE = 0x3
};
struct flags
{
Flag flag_1;
Flag flag_2;
Flag flag_3;
Flag flag_4;
};
int main()
{
flags my_flags;
my_flags.flag_1 = Flag::F_TRUE;
my_flags.flag_2 = Flag::F_FALSE;
my_flags.flag_3 = Flag::F_DEFAULT;
my_flags.flag_4 = Flag::F_TOGGLE;
std::cout << "size of flags: " << sizeof(flags) << "\n";
std::cout << (int)(my_flags.flag_1) << "\n";
std::cout << (int)(my_flags.flag_2) << "\n";
std::cout << (int)(my_flags.flag_3) << "\n";
std::cout << (int)(my_flags.flag_4) << "\n";
}
在这里,我们得到以下输出:
size of flags: 4
0
1
2
3
这种方式的内存效率不高。每个标志是 8 位,而两个布尔值各占一位,内存增加了 4 倍。但是,enum class
的 we are afforded the benefits 可以防止一些愚蠢的程序员错误。
现在,当内存很重要时,我有另一种解决方案。这里我们将 4 个标志打包到一个 8 位结构中。这是我为数据编辑器设计的,非常适合我的使用。然而,我现在意识到可能存在缺点。
// Example program
#include <iostream>
#include <string>
enum Flag
{
F_TRUE = 0x0, // Explicitly defined for readability
F_FALSE = 0x1,
F_DEFAULT = 0x2,
F_TOGGLE = 0x3
};
struct PackedFlags
{
public:
bool flag_1_0:1;
bool flag_1_1:1;
bool flag_2_0:1;
bool flag_2_1:1;
bool flag_3_0:1;
bool flag_3_1:1;
bool flag_4_0:1;
bool flag_4_1:1;
public:
Flag GetFlag1()
{
return (Flag)(((int)flag_1_1 << 1) + (int)flag_1_0);
}
Flag GetFlag2()
{
return (Flag)(((int)flag_2_1 << 1) + (int)flag_2_0);
}
Flag GetFlag3()
{
return (Flag)(((int)flag_3_1 << 1) + (int)flag_3_0);
}
Flag GetFlag4()
{
return (Flag)(((int)flag_4_1 << 1) + (int)flag_4_0);
}
void SetFlag1(Flag flag)
{
flag_1_0 = (flag & (1 << 0));
flag_1_1 = (flag & (1 << 1));
}
void SetFlag2(Flag flag)
{
flag_2_0 = (flag & (1 << 0));
flag_2_1 = (flag & (1 << 1));
}
void SetFlag3(Flag flag)
{
flag_3_0 = (flag & (1 << 0));
flag_3_1 = (flag & (1 << 1));
}
void SetFlag4(Flag flag)
{
flag_4_0 = (flag & (1 << 0));
flag_4_1 = (flag & (1 << 1));
}
};
int main()
{
PackedFlags my_flags;
my_flags.SetFlag1(F_TRUE);
my_flags.SetFlag2(F_FALSE);
my_flags.SetFlag3(F_DEFAULT);
my_flags.SetFlag4(F_TOGGLE);
std::cout << "size of flags: " << sizeof(my_flags) << "\n";
std::cout << (int)(my_flags.GetFlag1()) << "\n";
std::cout << (int)(my_flags.GetFlag2()) << "\n";
std::cout << (int)(my_flags.GetFlag3()) << "\n";
std::cout << (int)(my_flags.GetFlag4()) << "\n";
}
输出:
size of flags: 1
0
1
2
3
我目前正在尝试想出一种巧妙的方法来实现标志,除了通常的 "true" 和 "false".
标志的一般问题是,一个人有一个函数并想通过传递某些参数来定义它的行为("do something" 或 "don't do something")。
单旗
使用单个(布尔)标志,解决方案很简单:
void foo(...,bool flag){
if(flag){/*do something*/}
}
这里添加默认值特别容易,只需将函数更改为
void foo(...,bool flag=true)
并在没有标志参数的情况下调用它。
多个标志
一旦标志数量增加,我通常看到和使用的解决方案是这样的:
typedef int Flag;
static const Flag Flag1 = 1<<0;
static const Flag Flag2 = 1<<1;
static const Flag Flag3 = 1<<2;
void foo(/*other arguments ,*/ Flag f){
if(f & Flag1){/*do whatever Flag1 indicates*/}
/*check other flags*/
}
//call like this:
foo(/*args ,*/ Flag1 | Flag3)
这样做的好处是,您不需要为每个标志设置一个参数,这意味着用户可以设置他喜欢的标志,而忘记那些他不关心的标志。特别是你不会接到像 foo (/*args*/, true, false, true)
这样的电话,你必须计算哪个 true/false 表示哪个标志。
这里的问题是:
如果您设置了默认参数,一旦用户指定任何标志,它就会被覆盖。不可能像 Flag1=true, Flag2=false, Flag3=default
.
显然,如果我们想要有 3 个选项(真、假、默认),我们需要为每个标志传递至少 2 位。因此,虽然它可能不是必需的,但我想任何实现都应该很容易使用第 4 状态来指示切换(=!默认)。
我有两种方法,但我对这两种方法都不满意:
方法 1:定义 2 个标志
到目前为止,我尝试过使用这样的东西:
typedef int Flag;
static const Flag Flag1 = 1<<0;
static const Flag Flag1False= 1<<1;
static const Flag Flag1Toggle = Flag1 | Flag1False;
static const Flag Flag2= 1<<2;
static const Flag Flag2False= 1<<3;
static const Flag Flag2Toggle = Flag2 | Flag2False;
void applyDefault(Flag& f){
//do nothing for flags with default false
//for flags with default true:
f = ( f & Flag1False)? f & ~Flag1 : f | Flag1;
//if the false bit is set, it is either false or toggle, anyway: clear the bit
//if its not set, its either true or default, anyway: set
}
void foo(/*args ,*/ Flag f){
applyDefault(f);
if (f & Flag1) //do whatever Flag1 indicates
}
但是我不喜欢的是,每个标志使用两个不同的位。这导致 "default-true" 和 "default-false" 标志的代码不同,并且导致必要的 if 而不是 applyDefault()
.
方法 2:模板
通过这样定义模板-class:
struct Flag{
virtual bool apply(bool prev) const =0;
};
template<bool mTrue, bool mFalse>
struct TFlag: public Flag{
inline bool apply(bool prev) const{
return (!prev&&mTrue)||(prev&&!mFalse);
}
};
TFlag<true,false> fTrue;
TFlag<false,true> fFalse;
TFlag<false,false> fDefault;
TFlag<true,true> fToggle;
我能够将 apply
压缩成一个按位运算,除了 1 个参数之外的所有参数在编译时都是已知的。因此,使用 TFlag::apply
直接编译(使用 gcc)为与 return true;
、return false;
、return prev;
或 return !prev;
相同的机器代码,这是非常有效的,但这意味着如果我想将 TFlag
作为参数传递,我必须使用模板函数。从 Flag
继承并使用 const Flag&
作为参数增加了虚函数调用的开销,但使我免于使用模板。
但是我不知道如何将其扩展到多个标志...
问题
所以问题是: 我如何在 C++ 中的单个参数中实现多个标志,以便用户可以轻松地将它们设置为 "true"、"false" 或 "default"(通过不设置特定标志)或(可选) 表示 "whatever is not default"?
class 是否有两个整数,使用类似的按位运算,如模板方法和自己的按位运算符?如果是这样,有没有办法让编译器可以选择在编译时执行大部分按位运算?
编辑澄清:
我不想将 4 个不同的标志 "true"、"false"、"default"、"toggle" 传递给函数。
例如。想一想在标记用于 "draw border"、"draw center"、"draw fill color"、"blurry border"、"let the circle hop up and down"、"do whatever other fancy stuff you can do with a circle" 的地方绘制的圆圈。 ...
对于那些 "properties" 中的每一个,我想传递一个值为 true、false、default 或 toggle 的标志。
因此函数可能决定默认绘制边框、填充颜色和中心,但 none 其余部分。一个调用,大致是这样的:
draw_circle (DRAW_BORDER | DONT_DRAW_CENTER | TOGGLE_BLURRY_BORDER) //or
draw_circle (BORDER=true, CENTER=false, BLURRY=toggle)
//or whatever nice syntax you come up with....
应该绘制边框(由标志指定),不绘制中心(由标志指定),模糊边框(标志表示:不是默认值)并绘制填充颜色(未指定,但它是默认值) .
如果我后来决定不再默认绘制中心而是默认模糊边框,调用应该绘制边框(由标志指定),而不是绘制中心(由标志指定),not 模糊边框(现在模糊是默认的,但我们不想要默认)并绘制填充颜色(没有标志,但它是默认的)。
如果我理解这个问题,您可以通过使用 bool
的隐式构造函数和默认构造函数创建一个简单的 class 来解决问题:
class T
{
T(bool value):def(false), value(value){} // implicit constructor from bool
T():def(true){}
bool def; // is flag default
bool value; // flag value if flag isn't default
}
并像这样在函数中使用它:
void f(..., T flag = T());
void f(..., true); // call with flag = true
void f(...); // call with flag = default
不是很漂亮,但非常简单(从您的方法 1 构建):
#include <iostream>
using Flag = int;
static const Flag Flag1 = 1<<0;
static const Flag Flag2 = 1<<2;
// add more flags to turn things off, etc.
class Foo
{
bool flag1 = true; // default true
bool flag2 = false; // default false
void applyDefault(Flag& f)
{
if (f & Flag1)
flag1 = true;
if (f & Flag2)
flag2 = true;
// apply off flags
}
public:
void operator()(/*args ,*/ Flag f)
{
applyDefault(f);
if (flag1)
std::cout << "Flag 1 ON\n";
if (flag2)
std::cout << "Flag 2 ON\n";
}
};
void foo(/*args ,*/ Flag flags)
{
Foo f;
f(flags);
}
int main()
{
foo(Flag1); // Flag1 ON
foo(Flag2); // Flag1 ON\nFlag2 ON
foo(Flag1 | Flag2); // Flag1 ON\nFlag2 ON
return 0;
}
您的评论和回答让我找到了一个我喜欢并想与您分享的解决方案:
struct Default_t{} Default;
struct Toggle_t{} Toggle;
struct FlagSet{
uint m_set;
uint m_reset;
constexpr FlagSet operator|(const FlagSet other) const{
return {
~m_reset & other.m_set & ~other.m_reset |
~m_set & other.m_set & other.m_reset |
m_set & ~other.m_set,
m_reset & ~other.m_reset |
~m_set & ~other.m_set & other.m_reset|
~m_reset & other.m_set & other.m_reset};
}
constexpr FlagSet& operator|=(const FlagSet other){
*this = *this|other;
return *this;
}
};
struct Flag{
const uint m_bit;
constexpr FlagSet operator= (bool val) const{
return {(uint)val<<m_bit,(!(uint)val)<<m_bit};
}
constexpr FlagSet operator= (Default_t) const{
return {0u,0u};
}
constexpr FlagSet operator= (Toggle_t) const {
return {1u<<m_bit,1u<<m_bit};
}
constexpr uint operator& (FlagSet i) const{
return i.m_set & (1u<<m_bit);
}
constexpr operator FlagSet() const{
return {1u<<m_bit,0u}; //= set
}
constexpr FlagSet operator|(const Flag other) const{
return (FlagSet)*this|(FlagSet)other;
}
constexpr FlagSet operator|(const FlagSet other) const{
return (FlagSet)*this|other;
}
};
constexpr uint operator& (FlagSet i, Flag f){
return f & i;
}
所以基本上 FlagSet
包含两个整数。一个用于设置,一个用于重置。不同的组合代表该特定位的不同操作:
{false,false} = Default (D)
{true ,false} = Set (S)
{false,true } = Reset (R)
{true ,true } = Toggle (T)
operator|
使用了相当复杂的按位运算,旨在满足
D|D = D
D|R = R|D = R
D|S = S|D = S
D|T = T|D = T
T|T = D
T|R = R|T = S
T|S = S|T = R
S|S = S
R|R = R
S|R = S (*)
R|S = R (*)
(*) 中的非交换行为是由于这样一个事实,即我们不知何故需要能够决定哪个是 "default" 哪个是 "user defined"。所以如果值有冲突,左边的优先。
Flag
class 代表一个标志,基本上是一个位。使用不同的 operator=()
重载可以使某种 "Key-Value-Notation" 直接转换为 FlagSet,其中 m_bit
位置的位对设置为先前定义的对之一。默认情况下 (operator FlagSet()
) 这将转换为给定位上的 Set(S) 操作。
class 还为按位或自动转换为 FlagSet
和 operator&()
提供了一些重载,以实际比较 Flag
和 FlagSet
。在此比较中,Set(S) 和 Toggle(T) 都被视为 true
,而 Reset(R) 和 Default(D) 都被视为 false
.
使用它非常简单并且非常接近 "usual" 标志实现:
constexpr Flag Flag1{0};
constexpr Flag Flag2{1};
constexpr Flag Flag3{2};
constexpr auto NoFlag1 = (Flag1=false); //Just for convenience, not really needed;
void foo(FlagSet f={0,0}){
f |= Flag1|Flag2; //This sets the default. Remember: default right, user left
cout << ((f & Flag1)?"1":"0");
cout << ((f & Flag2)?"2":"0");
cout << ((f & Flag3)?"3":"0");
cout << endl;
}
int main() {
foo();
foo(Flag3);
foo(Flag3|(Flag2=false));
foo(Flag3|NoFlag1);
foo((Flag1=Toggle)|(Flag2=Toggle)|(Flag3=Toggle));
return 0;
}
输出:
120
123
103
023
003
关于效率的最后一句话:虽然我没有在没有所有 constexpr
关键字的情况下对其进行测试,但有了这些代码:
bool test1(){
return Flag1&((Flag1=Toggle)|(Flag2=Toggle)|(Flag3=Toggle));
}
bool test2(){
FlagSet f = Flag1|Flag2 ;
return f & Flag1;
}
bool test3(FlagSet f){
f |= Flag1|Flag2 ;
return f & Flag1;
}
编译为(在 gcc.godbolt.org 上使用 gcc 5.3)
test1():
movl , %eax
ret
test2():
movl , %eax
ret
test3(FlagSet):
movq %rdi, %rax
shrq , %rax
notl %eax
andl , %eax
ret
虽然我并不完全熟悉汇编程序代码,但这看起来像是非常基本的按位运算,并且可能是您在不内联测试函数的情况下可以获得的最快速度。
如果我理解正确,您需要一种简单的方法将一个或多个标志作为单个参数传递给函数,and/or一个对象跟踪一个或多个标志的简单方法单变量,对吗?一种简单的方法是将标志指定为类型化枚举,并使用足够大的无符号基础类型来容纳您需要的所有标志。例如:
/* Assuming C++11 compatibility. If you need to work with an older compiler, you'll have
* to manually insert the body of flag() into each BitFlag's definition, and replace
* FLAG_INVALID's definition with something like:
* FLAG_INVALID = static_cast<flag_t>(-1) -
* (FFalse + FTrue + FDefault + FToggle),
*/
#include <climits>
// For CHAR_BIT.
#include <cstdint>
// For uint8_t.
// Underlying flag type. Change as needed. Should remain unsigned.
typedef uint8_t flag_t;
// Helper functions, to provide cleaner syntax to the enum.
// Not actually necessary, they'll be evaluated at compile time either way.
constexpr flag_t flag(int f) { return 1 << f; }
constexpr flag_t fl_validate(int f) {
return (f ? (1 << f) + fl_validate(f - 1) : 1);
}
constexpr flag_t register_invalids(int f) {
// The static_cast is a type-independent maximum value for unsigned ints. The compiler
// may or may not complain.
// (f - 1) compensates for bits being zero-indexed.
return static_cast<flag_t>(-1) - fl_validate(f - 1);
}
// List of available flags.
enum BitFlag : flag_t {
FFalse = flag(0), // 0001
FTrue = flag(1), // 0010
FDefault = flag(2), // 0100
FToggle = flag(3), // 1000
// ...
// Number of defined flags.
FLAG_COUNT = 4,
// Indicator for invalid flags. Can be used to make sure parameters are valid, or
// simply to mask out any invalid ones.
FLAG_INVALID = register_invalids(FLAG_COUNT),
// Maximum number of available flags.
FLAG_MAX = sizeof(flag_t) * CHAR_BIT
};
// ...
void func(flag_t f);
// ...
class CL {
flag_t flags;
// ...
};
请注意,这假设 FFalse
和 FTrue
应该是不同的标志,两者可以同时指定。如果您希望它们相互排斥,则需要进行一些小改动:
// ...
constexpr flag_t register_invalids(int f) {
// Compensate for 0th and 1st flags using the same bit.
return static_cast<flag_t>(-1) - fl_validate(f - 2);
}
// ...
enum BitFlag : flag_t {
FFalse = 0, // 0000
FTrue = flag(0), // 0001
FDefault = flag(1), // 0010
FToggle = flag(2), // 0100
// ...
或者,您可以修改 flag()
:
enum
本身
// ...
constexpr flag_t flag(int f) {
// Give bit 0 special treatment as "false", shift all other flags down to compensate.
return (f ? 1 << (f - 1) : 0);
}
// ...
constexpr flag_t register_invalids(int f) {
return static_cast<flag_t>(-1) - fl_validate(f - 2);
}
// ...
enum BitFlag : flag_t {
FFalse = flag(0), // 0000
FTrue = flag(1), // 0001
FDefault = flag(2), // 0010
FToggle = flag(3), // 0100
// ...
虽然我认为这是最简单的方法,并且如果您为 flag_t
选择尽可能小的基础类型,则可能是内存效率最高的方法,但它也可能是最没有用的。 [此外,如果您最终使用这个或类似的东西,我建议将辅助函数隐藏在命名空间中,以防止全局命名空间出现不必要的混乱。]
为什么我们不能为此使用枚举?这是我最近使用的解决方案:
// Example program
#include <iostream>
#include <string>
enum class Flag : int8_t
{
F_TRUE = 0x0, // Explicitly defined for readability
F_FALSE = 0x1,
F_DEFAULT = 0x2,
F_TOGGLE = 0x3
};
struct flags
{
Flag flag_1;
Flag flag_2;
Flag flag_3;
Flag flag_4;
};
int main()
{
flags my_flags;
my_flags.flag_1 = Flag::F_TRUE;
my_flags.flag_2 = Flag::F_FALSE;
my_flags.flag_3 = Flag::F_DEFAULT;
my_flags.flag_4 = Flag::F_TOGGLE;
std::cout << "size of flags: " << sizeof(flags) << "\n";
std::cout << (int)(my_flags.flag_1) << "\n";
std::cout << (int)(my_flags.flag_2) << "\n";
std::cout << (int)(my_flags.flag_3) << "\n";
std::cout << (int)(my_flags.flag_4) << "\n";
}
在这里,我们得到以下输出:
size of flags: 4
0
1
2
3
这种方式的内存效率不高。每个标志是 8 位,而两个布尔值各占一位,内存增加了 4 倍。但是,enum class
的 we are afforded the benefits 可以防止一些愚蠢的程序员错误。
现在,当内存很重要时,我有另一种解决方案。这里我们将 4 个标志打包到一个 8 位结构中。这是我为数据编辑器设计的,非常适合我的使用。然而,我现在意识到可能存在缺点。
// Example program
#include <iostream>
#include <string>
enum Flag
{
F_TRUE = 0x0, // Explicitly defined for readability
F_FALSE = 0x1,
F_DEFAULT = 0x2,
F_TOGGLE = 0x3
};
struct PackedFlags
{
public:
bool flag_1_0:1;
bool flag_1_1:1;
bool flag_2_0:1;
bool flag_2_1:1;
bool flag_3_0:1;
bool flag_3_1:1;
bool flag_4_0:1;
bool flag_4_1:1;
public:
Flag GetFlag1()
{
return (Flag)(((int)flag_1_1 << 1) + (int)flag_1_0);
}
Flag GetFlag2()
{
return (Flag)(((int)flag_2_1 << 1) + (int)flag_2_0);
}
Flag GetFlag3()
{
return (Flag)(((int)flag_3_1 << 1) + (int)flag_3_0);
}
Flag GetFlag4()
{
return (Flag)(((int)flag_4_1 << 1) + (int)flag_4_0);
}
void SetFlag1(Flag flag)
{
flag_1_0 = (flag & (1 << 0));
flag_1_1 = (flag & (1 << 1));
}
void SetFlag2(Flag flag)
{
flag_2_0 = (flag & (1 << 0));
flag_2_1 = (flag & (1 << 1));
}
void SetFlag3(Flag flag)
{
flag_3_0 = (flag & (1 << 0));
flag_3_1 = (flag & (1 << 1));
}
void SetFlag4(Flag flag)
{
flag_4_0 = (flag & (1 << 0));
flag_4_1 = (flag & (1 << 1));
}
};
int main()
{
PackedFlags my_flags;
my_flags.SetFlag1(F_TRUE);
my_flags.SetFlag2(F_FALSE);
my_flags.SetFlag3(F_DEFAULT);
my_flags.SetFlag4(F_TOGGLE);
std::cout << "size of flags: " << sizeof(my_flags) << "\n";
std::cout << (int)(my_flags.GetFlag1()) << "\n";
std::cout << (int)(my_flags.GetFlag2()) << "\n";
std::cout << (int)(my_flags.GetFlag3()) << "\n";
std::cout << (int)(my_flags.GetFlag4()) << "\n";
}
输出:
size of flags: 1
0
1
2
3