5

私は現在、通常の「true」と「false」に加えて、「デフォルト」と (オプション) 「トグル」の状態を含むフラグを実装する賢い方法を考え出そうとしています。

フラグの一般的な問題は、関数があり、特定のパラメーターを渡すことによってその動作 (「何かを行う」または「何かをしない」) を定義したいということです。

シングルフラグ

単一の(ブール値)フラグを使用すると、ソリューションは簡単です。

void foo(...,bool flag){
    if(flag){/*do something*/}
}

ここでは、関数を次のように変更するだけで、デフォルトを追加するのが特に簡単です。

void foo(...,bool flag=true)

flag パラメータなしで呼び出します。

複数のフラグ

フラグの数が増えると、私が通常目にして使用するソリューションは次のようになります。

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 つのオプション (true、false、default) が必要な場合は、フラグごとに少なくとも 2 ビットを渡す必要があります。したがって、必要ではないかもしれませんが、どの実装でも 4 番目の状態を使用してトグル (= !default) を示すのは簡単だと思います。

これには2つのアプローチがありますが、どちらにも満足していません。

アプローチ 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
}

ただし、これについて私が気に入らないのは、各フラグに 2 つの異なるビットが使用されていることです。これにより、「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 つを除くすべての引数を使用して、 を 1 つのビット単位の演算に凝縮することができました。したがって、を (gcc を使用して) を使用して、、またはwouldTFlag::applyと同じマシン コードに直接コンパイルすると、非常に効率的ですが、引数としてa を渡したい場合は、テンプレート関数を使用する必要があります。引数を継承して使用すると、仮想関数呼び出しのオーバーヘッドが追加されますが、テンプレートを使用する必要がなくなります。return true;return false;return prev;return !prev;TFlagFlagconst Flag&

ただし、これを複数のフラグにスケールアップする方法がわかりません...

質問

問題は次のとおりです。C++ の 1 つの引数に複数のフラグを実装して、ユーザーが簡単に「true」、「false」、または「default」(特定のフラグを設定しないことで) または (オプション) に設定できるようにするにはどうすればよいですか? 「デフォルトではないもの」を示しますか?

独自のビット単位演算子を使用したテンプレート アプローチのような同様のビット単位操作を使用して、2 つの int を持つクラスを使用する方法はありますか? もしそうなら、コンパイル時にほとんどのビット演算を実行するオプションをコンパイラに与える方法はありますか?

明確にするために編集: 「true」、「false」、「default」、「toggle」の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....

境界線を描画し (フラグで指定)、中心を描画せず (フラグで指定)、境界線をぼやけさせ (フラグはデフォルトではないと言っています)、塗りつぶしの色を描画します (指定されていませんが、デフォルト)。
後でデフォルトで中心を描画せず、デフォルトで境界をぼかすことにした場合、呼び出しは境界を描画し(フラグで指定)、中心を描画せず(フラグで指定)、境界をぼかす必要はありません(現在はぼかしがデフォルトです) 、ただしデフォルトは必要ありません) 塗りつぶしの色を描画します (フラグはありませんが、デフォルトです)。

4

5 に答える 5

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;
}

したがって、基本的にFlagSetは 2 つの整数を保持します。1 つはセット用、もう 1 つはリセット用です。さまざまな組み合わせは、その特定のビットのさまざまなアクションを表します。

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

(*) の非交換動作は、どちらが「デフォルト」でどちらが「ユーザー定義」であるかを決定する機能が何らかの形で必要であるという事実によるものです。したがって、値が競合する場合は、左側の値が優先されます。

このFlagクラスは、基本的にビットの 1 つである単一のフラグを表します。さまざまなoperator=()オーバーロードを使用すると、ある種の "Key-Value-Notation" をm_bit、以前に定義されたペアの 1 つに設定された位置にあるビット ペアを持つ FlagSet に直接変換できます。デフォルト ( operator FlagSet()) では、これは指定されたビットの Set(S) アクションに変換されます。このクラスは、 を に自動変換し、実際に と比較する
ビットごとの OR のオーバーロードもいくつか提供します。この比較では、Set(S) と Toggle(T)の両方が考慮され、Reset(R) と Default(D) の両方が考慮されます。FlagSetoperator&()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;
}

出力:

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    $1, %eax
    ret
test2():
    movl    $1, %eax
    ret
test3(FlagSet):
    movq    %rdi, %rax
    shrq    $32, %rax
    notl    %eax
    andl    $1, %eax
    ret

私は Assembler-Code に完全に精通しているわけではありませんが、これは非常に基本的なビット単位の操作のように見え、おそらくテスト関数をインライン化せずに取得できる最速のものです。

于 2016-06-22T19:02:48.463 に答える
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;
}
于 2016-06-22T18:05:05.910 に答える
0

質問が理解できれば、暗黙のコンストラクター fromboolとデフォルトのコンストラクターを持つ単純なクラスを作成することで問題を解決できます。

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
于 2016-06-22T13:55:27.713 に答える
0

これに列挙型を使用できない理由はありますか? 最近使用したソリューションは次のとおりです。

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

この方法では、メモリ効率があまり良くありません。各フラグは、2 つの bool がそれぞれ 1 ビットであるのに対して 8 ビットであり、メモリが 4 倍増加します。ただし、プログラマーの愚かな間違いを防ぐという利点があります。enum 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
于 2016-06-22T21:34:37.590 に答える
0

私の理解が正しければ、関数に 1 つ以上のフラグを 1 つのパラメーターとして渡す簡単な方法や、オブジェクトが 1 つ以上のフラグを 1 つの変数で追跡する簡単な方法が必要ですよね?簡単なアプローチは、必要なすべてのフラグを保持するのに十分な大きさの符号なしの基になる型を使用して、型付き列挙型としてフラグを指定することです。例えば:

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

または、それ自体を変更する代わりに、次のようにenum変更できますflag()

// ...
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役に立たないでしょう。[また、これまたは類似のものを使用することになった場合は、ヘルパー関数を名前空間に隠して、グローバル名前空間が不要に混乱するのを防ぐことをお勧めします。]

簡単な例。

于 2016-06-22T21:13:13.977 に答える