Войти
ПрограммированиеФорумОбщее

С++: рукопожатно ли использовать конверсию в enum из значений, изначально в него не входящих?

Страницы: 1 2 Следующая »
#0
0:04, 26 окт. 2018

Есть некий идентификатор свойства. Сколько-то идентификаторов являются предопределёнными и зашиты в библиотеку. Ещё сколько-то будет доопределено юзером и зашито в код юзера.
Использование идентификаторов предполагается следующим образом:

class UserObject: public ObjectInterface
{
 ...
 /*override*/ void SetProperty (PropertyId pi, Value v)
 {
  switch (pi)
  {
  case PIBuiltinA: ... /* set builtinA */ break;
  case PIBuiltinB: ... /* set builtinB */ break;
  ...
  case PIUserA: ... /* set userA */ break;
  case PIUserB: ... /* set userB */ break;
  ...
  }
  ... // неизвестное свойство
 }
}
Этого можно добиться, сделав PropertyId на основе int. Но тогда можно будет сделать так:
SetProperty(100500, ...);
а хотелось бы, чтобы не было можно.
Делать PropertyId классом нельзя - тогда не получится схема со switch/case. Напрашивается вывод сделать его енумом, но вот незадача: состав енума определяется по месту декларации, а нам требуется, чтобы юзер мог расширять имеющийся библиотечный. Как же быть?
Применяем следующий приём:
// lib code
enum PropertyId
{
    PIMin = 0,
    PIBuiltinA = 1,
    PIBuiltinB = 2,
    PIFirstUser = 3,
    PIMax = ~((int)0) // чтобы гарантировать размер енума не меньше int
};

// user code
static const PropertyId PIUserA = PropertyId((int)PIFirstUser + 1);
static const PropertyId PIUserB = PropertyId((int)PIFirstUser + 2);

void test (PropertyId f)
{
    switch (f)
    {
    case PIBuiltinA: printf ("BuiltinA\n"); break;
    case PIBuiltinB: printf ("BuiltinB\n"); break;
    case PIUserA: printf ("UserA\n"); break;
    case PIUserB: printf ("UserB\n"); break;
    default: break;
    }
    return;
}

int main ()
{
    test(PIBuiltinA);
    test(PIBuiltinB);
    test(PIUserA);
    //test(1); // ошибка
}
На gcc, кланге, icc и m$vc фокус работает, и вроде бы профит. Но это похоже на утончённый хак. Посему вопрос: насколько такое рукопожатно и гарантировано по поведению с точки зрения стандарта?


#1
0:12, 26 окт. 2018

Когда у тебя в переменной значение, не входящее в енум, оно проскользнёт мимо проверок которые ожидают значения из енума и попадёт в `default`. А так то ну что, число в переменной, эка невидаль, в моих переменных тыщу раз числа бывали.

#2
0:40, 26 окт. 2018

Sbtrn. Devil
судя по stackoverflow да, можно, главное чтоб в базовый тип влезало. https://stackoverflow.com/questions/33812998/is-it-allowed-for-an… nlisted-value

#3
5:24, 26 окт. 2018

1) Выкинуть switch, и использовать std::unordered_map<std::string, std::function<void(Value)>>. Свитч не обязательно соберется во что-то эффективное: в худшем случае это будет простыня из if-else-if-...-else.

Максимальная читабельность кода и удобность дебага. По производительности не сильно хуже выйдет, а если в одном объекте достаточно много пропертей — даже лучше.

2) ProprtyID заменить строкой. switch делать по std::hash<std::string>()(propertyName).
Если уж так сильно хочется свитч. Если боишься коллизий: учитывая, например, что свойства состоят из [A-Za-z0-9_]+ можно сделать свой более эффективный хеш.

3) В 2018-ом не пользоваться  enum class — моветон.
Если будешь велосипедить дальше. И мучаться в дебаге: "А что за 7683? Напишу-ка я скрипт, который перед сборкой будет составлять список констант со значениями вне енама".

#4
(Правка: 5:50) 5:48, 26 окт. 2018

Sbtrn. Devil
> enum PropertyId
> {
> PIMin = 0,
> PIBuiltinA = 1,
> PIBuiltinB = 2,
> PIFirstUser = 3,
> PIMax = ~((int)0) // чтобы гарантировать размер енума не меньше int
> };
как вам там в 1998 живётся?

enum class PropertyId : int
#5
(Правка: 6:52) 6:52, 26 окт. 2018

mahrizh
> Свитч не обязательно соберется во что-то эффективное
Ты это сейчас с потолка взял, или у тебя есть реальные замеры?
unordered_map будет гарантированно менее эффективным, потому что switch/case можно оптимизировать на этапе компиляции, а отображение - нельзя.
> в худшем случае это будет простыня из if-else-if-...-else
Это почему ещё "в худшем"? Тоже части тела в 1998 застряли?

Ну и наконец - а с чего ты взял, что unordered_map окажется быстрее map в твоём конкретном случае?

> 2) ProprtyID заменить строкой.

+ Клоун?

#6
8:53, 26 окт. 2018

Delfigamer
> кудах ... динамическая типизация ... кудах
Я предложил идентифицировать проперти через строку, а не на джаваскрипте писать. Конечно строки нужно затолкать в именованные константы, чем тебе не проверка на опечатку?

Delfigamer
> unordered_map будет гарантированно менее эффективным

                  worst   average   best
unordered_map.at   O(N)     O(1)    O(1)
switch            O(logN)    хз    O(1)
Ок, согласен на нестрогое неравенство.

Тогда уж добавлю 4 вариант, если вы пишете для калькуляторов которые хеш от строчки считают годами:

typedef int PropertyId;
namespace property
{
    PropertyId const propertyA = 0;
    PropertyId const propertyB = 1;
    PropertyId const propertyC = 2;
}
Все равно придется следить какие числа использовались, а какие нет. Или заюзать что-то типа такого.
Но енам здесь точно не нужен, т.к. будет решать проблему только со встроенными пропертями, а пользователи этой либы будут страдать (чтобы разработчик понял свою ошибку на старте, а не когда начал писать кастомные проперти).

#7
(Правка: 9:27) 9:24, 26 окт. 2018

mahrizh
> Я предложил идентифицировать проперти через строку, а не на джаваскрипте
> писать. Конечно строки нужно затолкать в именованные константы, чем тебе не
> проверка на опечатку?
Можно и троллейбус сделать вкусным и питательным, если троллейбус - это буханка хлеба. Но зачем?

#8
9:59, 26 окт. 2018

Delfigamer
> Но зачем?
1) Дебажишь свой код. Переменная PropertyId randomPreoperty равна 1735.
2) Переходишь к описанию енама PropertyId, там только 20 значений.
3) Ищешь "PropertyId((int)PIFirstUser + ". Находишь еще 20 констант.
4) Повторяешь шаг (3) 1735/20-1 раз с разными хвостами констант.
5) ???
6) Кайфуешь от того, что код работает быстрее.

#9
10:37, 26 окт. 2018

n4727.pdf, §8.5.1.9:

10 A value of integral or enumeration type can be explicitly converted to a complete enumeration type. If the enumeration type has a fixed underlying type, the value is first converted to that type by integral conversion, if necessary, and then to the enumeration type. If the enumeration type does not have a fixed underlying type, the value is unchanged if the original value is within the range of the enumeration values (10.2), and otherwise, the behavior is undefined. A value of floating-point type can also be explicitly converted to an enumeration type. The resulting value is the same as converting the original value to the underlying type of the enumeration (7.10), and subsequently to the enumeration type.

§10.2:
8 For an enumeration whose underlying type is fixed, the values of the enumeration are the values of the underlying type. Otherwise, for an enumeration where emin is the smallest enumerator and emax is the largest, the values of the enumeration are the values in the range bmin to bmax, defined as follows: Let K be 1 for a two’s complement representation and 0 for a ones’ complement or sign-magnitude representation. bmax is the smallest value greater than or equal to max(|emin| − K, |emax|) and equal to 2M − 1, where M is a non-negative integer. bmin is zero if emin is non-negative and −(bmax + K) otherwise. The size of the smallest bit-field large enough to hold all the values of the enumeration type is max(M, 1) if bmin is zero and M + 1 otherwise. It is possible to define an enumeration that has values not defined by any of its enumerators. If the enumerator-list is empty, the values of the numeration are as if the enumeration had a single enumerator with value 0.98
98) This set of values is used to define promotion and conversion semantics for the enumeration type. It does not preclude an expression of enumeration type from having a value that falls outside this range.
#10
11:18, 26 окт. 2018

Suslik
> как вам там в 1998 живётся?
> enum class PropertyId : int
Угу. А теперь в ProperyId запихнуть также PIUserA и PIUserB, пжалста. Ну, шоб не было вот такого:

switch (f)
    {
    case PropertyId::PIBuiltinA: printf ("BuiltinA\n"); break;
    case PropertyId::PIBuiltinB: printf ("BuiltinB\n"); break;
    case PIUserA: printf ("UserA\n"); break;
    case PIUserB: printf ("UserB\n"); break;
    default: break;
    }
Что, нельзя? Тогда мы всё-таки лучше в 1998 посидим (точнее, в 2003, но не суть).

mahrizh
> switch делать по std::hash<std::string>()(propertyName).
А case по чему делать? Там только компайлтаймовые интегральные значения.

FordPerfect
> It is possible to define an enumeration that has values not defined by any of
> its enumerators.
Во, то что нужно.

#11
20:47, 26 окт. 2018

Sbtrn. Devil
> Делать PropertyId классом нельзя - тогда не получится схема со switch/case

operator unsigned int плюс раздельные константы и конструкторы констант, а также наследование для юзерского расширения

+ говнисто, да, но вроде обеспечиваются все хотелки из первопоста
#12
22:09, 26 окт. 2018

Мысли вслух.

Размер можно указывать и для классических перечислений: enum PropertyId:int32_t.

Компиляторы делают кучу забавных оптимизаций для switch. Линейную прогрессию (по непрерывному диапазону) они вычисляют арифметикой. Для небольших диапазонов используются таблицы (в т. ч. для несплошных). Разреженные диапазоны могут использовать branch tree (успешность branch prediction зависит от входных данных). Отсутствие returndefault) влияет на код (и выдаёт warning; впрочем того же можно добиться __builtin_unreachable()).

Вот чтобы switch использовал perfect hashing - не видел, кроме тривиального (n -> n).

case PIUserA: выдаёт warning (-Wswitch) на GCC, но не clang.
Явное case PIUserA:PropertyId((int)PIFirstUser + 2); выдаёт warning и там, и там.

Отсутствие возможности сказать using EnumName; для scoped enum мне кажется странной, т. к. я вообще не вижу чему оно мешало бы (о чём я говорил ещё в своей теме).

Вдогонку https://stackoverflow.com/questions/5402745/gcc-switch-on-enum-re… t-use-default .

Зачем в #0 default - непонятно.

Если хочется наследования, а встроенного наследования для enum в C++ нет, так можно что-то вроде

struct PropertyId {enum:int{PIBuiltinA=1,PIBuiltinB=2}; int value;};
struct PropertyIdUser:PropertyId {enum:int{PIUserA=3,PIUserB=4};};

#13
(Правка: 22:50) 22:46, 26 окт. 2018

mahrizh
> 3) В 2018-ом не пользоваться  enum class — моветон.
Suslik
> как вам там в 1998 живётся?
как человеку далёкому от крестов, объясните какая разница между "enum class" и "enum"?
сам компилятор разве не распарсит что это одно и то же? или это уже не одно и то же? (или всякие фичи классов, типа перегрузки операторов, с наследование уже не повесить?)

+ Показать
#14
23:42, 26 окт. 2018

Dmitry_Milk
> operator unsigned int() { return value; }
Тут получится обратная ситуация - можно будет сунуть MyEnum куда-нибудь вместо инта, а это может быть чревато.

Было б здорово, если б свитч умел по глобальным статическим указателям или функциям, как умеют шаблонопараметры... Но почему-то нет.

skalogryz
> объясните какая разница между "enum class" и "enum"?

enum A { A1 };
A a = A1; // типа некошерно и антиинтуитивно

enum class B { B1 };
B b = B::B1; // типа по-пацански
Страницы: 1 2 Следующая »
ПрограммированиеФорумОбщее