【问题标题】:How to implement flags with the options true,false,default and toggle in C++?如何在 C++ 中使用选项 true、false、default 和 toggle 实现标志?
【发布时间】:2016-06-22 13:38:23
【问题描述】:

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

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

单个标志

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

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) 这样的电话,你必须计算哪个真/假表示哪个标志。

这里的问题是:
如果您设置默认参数,则只要用户指定任何标志,它就会被覆盖。像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:模板

通过这样定义模板类:

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; 相同的机器代码,这非常有效,但这意味着我必须使用模板-functions 如果我想传递一个TFlag 作为参数。从Flag 继承并使用const Flag&amp; 作为参数会增加虚函数调用的开销,但可以避免使用模板。

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

问题

所以问题是: 我如何在 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....

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

【问题讨论】:

  • 为什么不使用一种标志图呢?然后您需要正确合并地图(默认和参数)以获取标志...
  • 与这些按位运算相比,映射不是效率极低吗?那么函数调用会是什么样子呢?
  • 我说的是一种,不一定是 C++ 映射。无论如何,人们可以想象用f(...,FlagMap(Flag1(true),Flag3(default),Flag17(toggle))); 或类似的方式打电话吗?也许是一个标志集?...
  • 我不确定它是否适合我的情况(阅读:如果我可以教那些将使用它的人使用这个系统),但使用这样的“键值对”是一个有趣的想法" 符号。
  • std::vector&lt;bool&gt; 呢?

标签: c++ flags bitflags


【解决方案1】:

不完全漂亮,但非常简单(根据您的方法 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;
}

【讨论】:

  • 我喜欢你把函数变成类的想法。我说得对吗,要关闭它,可以定义static const Flag NoFlag1 = 1&lt;&lt;1; 之类的内容并将if (f &amp; NoFlag1) flag1=false; 添加到applyDefault
  • 是的。公平地说,它不必使用类,但有时像我这样的懒惰程序员喜欢一次切换一个标志。简单的 POD 有帮助。
【解决方案2】:

您的 cmets 和回答为我指明了一个我喜欢并希望与您分享的解决方案:

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| 使用了相当复杂的按位运算,旨在填充​​p>

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  (*) 

(*) 中的非交换行为是由于这样一个事实,即我们以某种方式需要能够决定哪一个是“默认”,哪一个是“用户定义”。所以在值冲突的情况下,左边的优先。

Flag 类代表一个标志,基本上是一个位。使用不同的operator=() 重载可以使某种“键值符号”直接转换为标志集,其中m_bit 位置处的位对设置为先前定义的对之一。默认情况下 (operator FlagSet()) 这将转换为对给定位的 Set(S) 操作。
该类还为按位或自动转换为FlagSetoperator&amp;() 以实际比较FlagFlagSet 提供了一些重载。在此比较中,Set(S) 和 Toggle(T) 都被视为 true,而 Reset(R) 和 Default(D) 都被视为 false

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

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

Test it on 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;
}

编译为(在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

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

【讨论】:

  • 所以实际的标志是uintFlag 的运算符|= 将产生FlagSet,当与Flag 与运算符一起使用时将变为uint &amp;?
  • 是的,这就是它的简短版本。
【解决方案3】:

如果我理解了这个问题,您可以通过使用 bool 的隐式构造函数和默认构造函数创建一个简单的类来解决问题:

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

【讨论】:

  • 嗯,我不知道如何将它用于多个标志。我想避免为每个标志设置一个参数,并在“多个标志”中使用更接近版本的内容。那么与仅使用布尔值相比,这有什么好处呢?
  • 哦,我明白了。看来我没听懂问题。
  • 你想在 python 中拥有类似kwargs 的东西吗?我在基于模板的 c++ 中找到了它的实现。我不确定它是否足够快,但你可以看看:github.com/cheshirekow/kwargs
  • 这看起来很有趣
【解决方案4】:

如果我理解正确,您想要一种将一个或多个标志作为单个参数传递给函数的简单方法,和/或一种让对象跟踪单个变量中的一个或多个标志的简单方法,正确?一种简单的方法是将标志指定为类型化枚举,其无符号基础类型足够大以容纳您需要的所有标志。例如:

/* 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;

    // ...
};

请注意,这假定FFalseFTrue 应该是不同的标志,这两个标志可以同时指定。如果您希望它们相互排斥,则需要进行一些小的更改:

// ...
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 选择尽可能小的底层类型,它可能也是最节省内存的方法,但它也可能是最没用的。 [另外,如果你最终使用这个或类似的东西,我建议将帮助函数隐藏在命名空间中,以防止全局命名空间中不必要的混乱。]

A simple example.

【讨论】:

  • 感谢您的努力,但我认为您误解了我的问题,因为我希望能够为每个属性/位/标志/无论您如何称呼它指明四种状态。请参阅我的编辑和我的答案以获得进一步的说明。
  • @Anedar 啊,好的。很高兴你找到了适合你的东西。
【解决方案5】:

我们不能为此使用枚举有什么原因吗?这是我最近使用的一个解决方案:

// 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 倍。但是,we are afforded the benefitsenum class 可以防止一些愚蠢的程序员错误。

现在,当内存很重要时,我有另一种解决方案。在这里,我们将 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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-01-16
    • 1970-01-01
    • 1970-01-01
    • 2018-12-08
    • 1970-01-01
    • 2012-10-08
    • 2016-04-16
    • 2018-09-07
    相关资源
    最近更新 更多