В настоящее время я пытаюсь найти умный способ реализации флагов, которые включают состояния «по умолчанию» и (необязательно) «переключение» в дополнение к обычным «истина» и «ложь».
Общая проблема с флагами состоит в том, что каждый имеет функцию и хочет определить ее поведение (либо «что-то делать», либо «не делать что-то») путем передачи определенных параметров.
С одним (логическим) флагом решение простое:
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 подхода к этому, но я не очень доволен ими обоими:
Я пытался использовать что-то подобное до сих пор:
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()
,
Определив шаблон-класс следующим образом:
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....
следует нарисовать границу (указанную флагом), а не нарисовать центр (указанную флагом), размыть границу (флаг говорит: не по умолчанию) и нарисовать цвет заливки (не указан, но по умолчанию).
Если позже я решу больше не рисовать центр по умолчанию, а размыть границу по умолчанию, вызов должен нарисовать границу (указанную флагом), а не нарисовать центр (указанную флагом), не размыть границу (теперь размытие по умолчанию, но мы не хотим по умолчанию) и нарисовать цвет заливки (нет флага для него, но по умолчанию).
Не совсем красиво, но очень просто (построение из вашего подхода 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 (*)
Некоммутативное поведение в (*) связано с тем, что нам почему-то нужна возможность решить, какой из них «по умолчанию», а какой «определенный пользователем». Так что в случае противоречивых значений, левый имеет приоритет.
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
Последнее слово об эффективности: пока я не проверял это без всего 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
и хотя я не совсем знаком с ассемблерным кодом, это выглядит как очень простые побитовые операции и, вероятно, самые быстрые, которые вы можете получить, не вставляя тестовые функции.
Если я понимаю вопрос, вы можете решить эту проблему, создав простой класс с неявным конструктором из 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
Если я правильно понимаю, вы хотите простой способ передать один или несколько флагов в функцию в виде одного параметра и / или простой способ для объекта отслеживать один или несколько флагов в одной переменной, верно? Простым подходом было бы указать флаги как типизированное перечисление с базовым типом без знака, достаточно большим, чтобы вместить все необходимые флаги. Например:
/* 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
Это, вероятно, также наименее полезно. [Кроме того, если вы в конечном итоге используете это или что-то подобное, я бы предложил скрыть вспомогательные функции в пространстве имен, чтобы предотвратить ненужный беспорядок в глобальном пространстве имен.]
Есть ли причина, по которой мы не можем использовать перечисление для этого? Вот решение, которое я использовал недавно:
// 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