如何在 C++ 中使用选项 true、false、default 和 toggle 实现标志?

Ane*_*dar 5 c++ flags bitflags

我目前正在尝试提出一种巧妙的方法来实现标志,除了通常的“true”和“false”之外,还包括状态“default”和(可选)“toggle”。

标志的一般问题是,它有一个函数并希望通过传递某些参数来定义其行为(“做某事”或“不做某事”)。

单旗

使用单个(布尔值)标志,解决方案很简单:

void foo(...,bool flag){
    if(flag){/*do something*/}
}
Run Code Online (Sandbox Code Playgroud)

在这里添加默认值特别容易,只需将函数更改为

void foo(...,bool flag=true)
Run Code Online (Sandbox Code Playgroud)

并在没有标志参数的情况下调用它。

多个标志

一旦标志数量增加,我通常看到和使用的解决方案是这样的:

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)
Run Code Online (Sandbox Code Playgroud)

这样做的好处是,您不需要每个标志的参数,这意味着用户可以设置他喜欢的标志,而只需忘记他不关心的标志。特别是你不会接到电话,比如foo (/*args*/, true, false, true)你必须计算哪个真/假表示哪个标志。

这里的问题是:
如果你设置了一个默认参数,一旦用户指定了任何标志,它就会被覆盖。不可能像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
}
Run Code Online (Sandbox Code Playgroud)

但是,我不喜欢的是,每个标志使用两个不同的位。这导致“default-true”和“default-false”标志的不同代码以及必要的 if 而不是applyDefault().

方法 2:模板

通过定义这样的模板类:

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;
Run Code Online (Sandbox Code Playgroud)

我能够将其压缩apply为单个按位运算,在编译时除了 1 个参数之外的所有参数都是已知的。因此,使用TFlag::apply直接编译(使用gcc)在同一台机器代码为return true;return false;return prev;return !prev;会,这是非常有效的,但是这意味着我必须使用模板的功能,如果我想传递一个TFlag作为参数。继承Flag和使用const Flag&as 参数会增加虚函数调用的开销,但可以避免使用模板。

但是我不知道如何将其扩展到多个标志......

所以问题是:如何在 C++ 中的单个参数中实现多个标志,以便用户可以轻松地将它们设置为“真”、“假”或“默认”(通过不设置特定标志)或(可选)表示“任何不是默认的”?

是一个有两个整数的类,使用类似的按位运算,比如模板方法,它自己的按位运算符是要走的路吗?如果是这样,有没有办法让编译器可以选择在编译时执行大部分按位运算?

编辑澄清: 我不想将 4 个不同的标志“true”、“false”、“default”、“toggle”传递给函数。
例如,想象一个圆圈被绘制在标志用于“绘制边框”、“绘制中心”、“绘制填充颜色”、“模糊边框”、“让圆圈上下跳跃”、“做任何其他幻想”的地方你可以用圆圈做的事情”,...。
对于这些“属性”中的每一个,我想传递一个值为真、假、默认或切换的标志。因此,该函数可能会决定默认绘制边框、填充颜色和居中,但其余的都不绘制。一个调用,大致是这样的:

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....
Run Code Online (Sandbox Code Playgroud)

应该绘制边框(由标志指定),而不是绘制中心(由标志指定),模糊边界(标志表示:不是默认值)并绘制填充颜色(未指定,但它的默认值)。
如果我后来决定不再默认绘制中心但默认模糊边框,则调用应绘制边框(由标志指定),不绘制中心(由标志指定),模糊边框(现在模糊是默认的) ,但我们不想要默认值)并绘制填充颜色(没有标志,但它的默认值)。

Ane*_*dar 1

您的评论和回答让我找到了一个我喜欢并想与您分享的解决方案:

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;
}
Run Code Online (Sandbox Code Playgroud)

所以基本上它FlagSet保存两个整数。一个用于设置,一个用于重置。不同的组合代表该特定位的不同操作:

{false,false} = Default (D)
{true ,false} = Set (S)
{false,true } = Reset (R)
{true ,true } = Toggle (T)
Run Code Online (Sandbox Code Playgroud)

使用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  (*) 
Run Code Online (Sandbox Code Playgroud)

(*) 中的非交换行为是因为我们需要能够确定哪一个是“默认”、哪一个是“用户定义”这一事实。因此,如果值发生冲突,则左侧的值优先。

该类Flag代表一个标志,基本上是一个位。使用不同的operator=()重载使得某种“键值表示法”能够直接转换为 FlagSet,并将位置处的位m_bit对设置为先前定义的对之一。默认情况下 ( operator FlagSet()) 这将转换为给定位上的 Set(S) 操作。
该类还提供了一些按位或的重载,自动转换为FlagSetoperator&()与. 在此比较中,同时考虑 Set(S) 和 Toggle(T) ,同时考虑 Reset(R) 和 Default(D) 。FlagFlagSettruefalse

使用它非常简单并且非常接近“通常的”标志实现:

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;
}
Run Code Online (Sandbox Code Playgroud)

输出:

120
123
103
023
003
Run Code Online (Sandbox Code Playgroud)

在ideone上测试一下

关于效率的最后一句话:虽然我没有在没有所有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;
}
Run Code Online (Sandbox Code Playgroud)

编译为(在gcc.godbolt.org上使用 gcc 5.3 )

test1():
    movl    $1, %eax
    ret
test2():
    movl    $1, %eax
    ret
test3(FlagSet):
    movq    %rdi, %rax
    shrq    $32, %rax
    notl    %eax
    andl    $1, %eax
    ret
Run Code Online (Sandbox Code Playgroud)

虽然我并不完全熟悉汇编代码,但这看起来像是非常基本的位运算,并且可能是在不内联测试函数的情况下可以获得的最快速度。