Как реализовать флаги с параметрами true, false, default и toggle в C ++?

В настоящее время я пытаюсь найти умный способ реализации флагов, которые включают состояния «по умолчанию» и (необязательно) «переключение» в дополнение к обычным «истина» и «ложь».

Общая проблема с флагами состоит в том, что каждый имеет функцию и хочет определить ее поведение (либо «что-то делать», либо «не делать что-то») путем передачи определенных параметров.

Единый флаг

С одним (логическим) флагом решение простое:

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 параметра (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
}

Однако что мне не нравится в этом, так это то, что для каждого флага используются два разных бита. Это приводит к различному коду для флагов «default-true» и «default-false» и к необходимости, если вместо какой-то хорошей побитовой операции в 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; будет, что довольно эффективно, но это будет означать, что я должен использовать функции-шаблоны, если я хочу передать TFlag в качестве аргумента. Наследование от Flag и используя const Flag& аргумент добавляет накладные расходы при вызове виртуальной функции, но избавляет меня от использования шаблонов.

Однако я понятия не имею, как масштабировать это до нескольких флагов …

Вопрос

Итак, вопрос:
Как я могу реализовать несколько флагов в одном аргументе в C ++, чтобы пользователь мог легко установить для них «true», «false» или «default» (не устанавливая определенный флаг) или (необязательно) указать «что бы не было» дефолт»?

Класс с двумя целочисленными значениями, использующий аналогичную побитовую операцию, подобную шаблонному подходу со своими собственными побитовыми операторами, — путь? И если да, есть ли способ дать компилятору возможность выполнять большинство побитовых операций во время компиляции?

Изменить для уточнения:
Я не хочу передавать 4 различных флага «true», «false», «default», «toggle» функции.
Например. представьте себе нарисованный круг, в котором флаги используются для «рисовать границу», «нарисовать центр», «нарисовать цвет заливки», «размыть границу», «позволить кругу прыгать вверх и вниз», «делать все остальное можно сделать с кружком «, ….
И для каждого из этих «свойств» я хочу передать флаг со значением 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....

следует нарисовать границу (указанную флагом), а не нарисовать центр (указанную флагом), размыть границу (флаг говорит: не по умолчанию) и нарисовать цвет заливки (не указан, но по умолчанию).
Если позже я решу больше не рисовать центр по умолчанию, а размыть границу по умолчанию, вызов должен нарисовать границу (указанную флагом), а не нарисовать центр (указанную флагом), не размыть границу (теперь размытие по умолчанию, но мы не хотим по умолчанию) и нарисовать цвет заливки (нет флага для него, но по умолчанию).

3

Решение

Не совсем красиво, но очень просто (построение из вашего подхода 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;
}
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 содержит два целых числа Один для установки, один для сброса. Различные комбинации представляют разные действия для этого конкретного бита:

{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 Класс представляет один флаг, в основном один из битов. Используя разные operator=() Перегрузки позволяют какой-то «Key-Value-Notation» преобразовывать непосредственно в FlagSet с парой битов в позиции m_bit установить одну из ранее определенных пар. По умолчанию (operator FlagSet()) это преобразуется в действие Set (S) для данного бита.
Класс также предоставляет некоторые перегрузки для побитового ИЛИ, которые автоматически преобразуются в FlagSet а также operator&() на самом деле сравнить Flag с FlagSet, В этом сравнении рассматриваются как 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

Проверьте это на 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 5.3 on gcc.godbolt.org)

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

и хотя я не совсем знаком с ассемблерным кодом, это выглядит как очень простые побитовые операции и, вероятно, самые быстрые, которые вы можете получить, не вставляя тестовые функции.

1

Если я понимаю вопрос, вы можете решить эту проблему, создав простой класс с неявным конструктором из 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
0

Если я правильно понимаю, вы хотите простой способ передать один или несколько флагов в функцию в виде одного параметра и / или простой способ для объекта отслеживать один или несколько флагов в одной переменной, верно? Простым подходом было бы указать флаги как типизированное перечисление с базовым типом без знака, достаточно большим, чтобы вместить все необходимые флаги. Например:

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

В качестве альтернативы, вместо изменения 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Это, вероятно, также наименее полезно. [Кроме того, если вы в конечном итоге используете это или что-то подобное, я бы предложил скрыть вспомогательные функции в пространстве имен, чтобы предотвратить ненужный беспорядок в глобальном пространстве имен.]

Простой пример

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

Это не совсем эффективно для памяти. Каждый флаг равен 8 битам по сравнению с двумя значениями bool в одном бите для увеличения памяти в 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
0
По вопросам рекламы ammmcru@yandex.ru
Adblock
detector