C / C ++: есть ли способ получить рефлексивные перечисления? - PullRequest
16 голосов
/ 26 ноября 2009

Я сталкивался с такой ситуацией много раз ...

 enum Fruit {
  Apple,
  Banana,
  Pear,
  Tomato
 };

Теперь у меня есть Fruit f; // banana, и я хочу перейти от f к строке "Banana"; или у меня есть string s = "Banana", и с этого я хочу перейти к Banana // enum value or int.

До сих пор я делал это .. Предполагая, что перечисление находится в Fruit.h:

// Fruit.cpp
const char *Fruits[] = {
 "Apple",
 "Banana",
 "Pear",
 "Tomato",
 NULL
};

Очевидно, что это грязное решение. Если разработчик добавляет в заголовок новый фрукт и не добавляет новую запись в Fruits [] (не могу его винить, они должны быть в двух разных файлах!), Приложение выходит из строя.

Есть ли простой способ сделать то, что я хочу, где все в одном файле? Препроцессорные хаки, инопланетная магия, что угодно ..

PS: Это, вопреки рефлексии "для всего", было бы очень просто реализовать в компиляторах. Видя, насколько распространена проблема (по крайней мере, для меня), я действительно не могу поверить, что нет reflective enum Fruit .. Даже в C ++ 0x.

PS2: я использую C ++, но я также пометил этот вопрос как C, потому что C имеет ту же проблему. Если ваше решение включает только C ++, это нормально для меня.

Ответы [ 10 ]

32 голосов
/ 26 ноября 2009

Этот требует, чтобы фрукты были определены во внешнем файле. Это будет содержимое fruit.cpp :

#define FRUIT(name) name
enum Fruit {
#include "fruit-defs.h"
NUM_FRUITS
};
#undef FRUIT
#define FRUIT(name) #name
const char *Fruits [] = {
#include "fruit-defs.h"
NULL
};
#undef FRUIT

И это будет fruit-defs.h :

FRUIT(Banana),
FRUIT(Apple),
FRUIT(Pear),
FRUIT(Tomato),

Это работает, пока значения начинаются с 0 и являются последовательными ...

Обновление: смешайте это решение с решением от Ричарда Пеннингтона, используя C99, если вам нужны непоследовательные значения. То есть что-то вроде:

// This would be in fruit-defs.h
FRUIT(Banana, 7)
...
// This one for the enum
#define FRUIT(name, number) name = number
....
// This one for the char *[]
#define FRUIT(name, number) [number] = #name
9 голосов
/ 26 ноября 2009

Способ c99, который я нашел, помогает уменьшить количество ошибок:

enum Fruit {
  APPLE,
  BANANA
};
const char* Fruits[] = {
 [APPLE] = "APPLE",
 [BANANA] = "BANANA"
};

Вы можете добавлять перечисления, даже в середине, и не нарушать старые определения. Конечно, вы можете получить пустые строки для значений, которые вы забыли.

4 голосов
/ 27 ноября 2009

Один комментарий к решению для макроса - вам не нужен отдельный файл для перечислителей. Просто используйте другой макрос:

#define FRUITs \ 
    FRUIT(Banana), \ 
    FRUIT(Apple), \ 
    FRUIT(Pear), \ 
    FRUIT(Tomato)

(хотя я бы, вероятно, пропустил запятые и при необходимости включил бы их в макрос FRUIT.)

4 голосов
/ 26 ноября 2009

Один трюк, который я сделал в прошлом, - это добавление дополнительного перечисления, а затем выполнение утверждения времени компиляции (например, Boost's ), чтобы убедиться, что они синхронизированы:

enum Fruit {
    APPLE,
    BANANA,

    // MUST BE LAST ENUM
    LAST_FRUIT
};

const char *FruitNames[] =
{
    "Apple",
    "Banana",
};

BOOST_STATIC_ASSERT((sizeof(FruitNames) / sizeof(*FruitNames)) == LAST_FRUIT);

Это, по крайней мере, помешает кому-то забыть добавить как перечисление, так и массив имен, и сообщит им, как только они попытаются скомпилировать.

3 голосов
/ 26 ноября 2009

Что если вы сделали что-то подобное?

enum Fruit {
  Apple,
  Banana,
  NumFruits
};

const char *Fruits[NumFruits] = {
 "Apple",
 "Banana",
};

Затем, если вы добавите новую запись в перечисление Fruit, ваш компилятор должен будет жаловаться на то, что в инициализаторе массива недостаточно записей, поэтому вам придется добавить запись в массив.

Таким образом, он защищает вас от неправильного размера массива, но не помогает гарантировать правильность строк.

1 голос
/ 21 января 2017

Существует также Better Enums , которая является библиотекой (файлом), предназначенной только для заголовков, которая требует C ++ 11 и лицензирована по лицензии BSD. Официальное описание:

Отражающие перечисления времени компиляции для C +: Better Enums - это один легкий облегченный заголовочный файл, который заставляет ваш компилятор генерировать отражающие типы перечислений.

Вот пример кода с официального сайта:

#include <enum.h>

BETTER_ENUM(Channel, int, Red = 1, Green, Blue)

Channel     c = Channel::_from_string("Red");
const char  *s = c._to_string();

size_t      n = Channel::_size();
for (Channel c : Channel::_values()) {
    run_some_function(c);
}

switch (c) {
    case Channel::Red:    // ...
    case Channel::Green:  // ...
    case Channel::Blue:   // ...
}

Channel     c = Channel::_from_integral(3);

constexpr Channel c =
    Channel::_from_string("Blue");

Это выглядит очень многообещающе, хотя я еще не проверял это. Кроме того, существует множество (пользовательских) библиотек отражений для C ++. Я надеюсь, что что-то похожее на Better Enums будет частью Стандартной библиотеки шаблонов (STL) (или, по крайней мере, Boost), рано или поздно.

1 голос
/ 11 августа 2015

Взгляните на библиотеку Metaresc https://github.com/alexanderchuranov/Metaresc

Предоставляет интерфейс для объявления типов, который также генерирует метаданные для типа. На основе метаданных вы можете легко сериализовать / десериализовать объекты любой сложности. Из коробки вы можете сериализовать / десериализовать XML, JSON, XDR, Lisp-подобную нотацию, нотацию C-init.

Вот простой пример:

#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>

#include "metaresc.h"

TYPEDEF_ENUM (fruit_t,
              Apple,
              Banana,
              Pear,
              Tomato,
              );

int main (int argc, char * argv[])
{
  mr_td_t * tdp = mr_get_td_by_name ("fruit_t");

  if (tdp)
    {
      int i;
      for (i = 0; i < tdp->fields_size / sizeof (tdp->fields[0]); ++i)
        printf ("[%" SCNd64 "] = %s\n", tdp->fields[i].fdp->param.enum_value, tdp->fields[i].fdp->name.str);
    }
  return (EXIT_SUCCESS);
}

Эта программа выведет

$ ./enum
[0] = Apple
[1] = Banana
[2] = Pear
[3] = Tomato

Библиотека прекрасно работает для последних версий gcc и clang.

1 голос
/ 26 ноября 2009

Мне вообще не нравятся макрорешения, хотя я признаю, что их довольно сложно избежать.

Лично я выбрал собственный класс для упаковки своих перечислений. Цель состояла в том, чтобы предложить немного больше, чем традиционные перечисления (например, итерации).

Под обложкой я использую std::map, чтобы сопоставить перечисление с его std::string аналогом. Затем я могу использовать это как для перебора перечисления, так и для «красивой печати» моего перечисления или для инициализации его из строки, прочитанной в файле.

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

Кроме того, я затем использую не реальное перечисление, а const_iterator, указывающий на карту (под обложками) для представления значения перечисления (с end, представляющим недопустимое значение).

1 голос
/ 26 ноября 2009

Может сделать для него структуру класса:

class Fruit { 
   int value; char const * name ; 
   protected:
   Fruit( int v, char const * n ) : value(v), name(n) {}
   public:
   int asInt() const { return value ; }
   char const * cstr() { return name ; } 
} ;
#define MAKE_FRUIT_ELEMENT( x, v ) class x : public Fruit { x() : Fruit( v, #x ) {} }

// Then somewhere:
MAKE_FRUIT_ELEMENT(Apple, 1);
MAKE_FRUIT_ELEMENT(Banana, 2);
MAKE_FRUIT_ELEMENT(Pear, 3);

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

void foo( Fruit f ) {
  std::cout << f.cstr() << std::endl;
  switch (f.asInt()) { /* do whatever * } ;
}

Размер этого в 2 раза больше, чем просто enum. Но, скорее всего, это не имеет значения.

1 голос
/ 26 ноября 2009

Как показали другие люди, отвечающие на этот вопрос, на самом деле не существует чистого ("D.R.Y.") способа сделать это, используя только препроцессор Си. Проблема в том, что вам нужно определить массив размера вашего перечисления, содержащий строки, соответствующие каждому значению перечисления, а препроцессор C не достаточно умен, чтобы это сделать. Что я делаю, это создаю текстовый файл примерно так:

%status ok
%meaning
The routine completed its work successfully.
%

%status eof_reading_content
%meaning

The routine encountered the end of the input before it expected
to. 

%

Здесь разделители меток%.

Затем Perl-скрипт, рабочая часть которого выглядит следующим образом:

sub get_statuses
{
    my ($base_name, $prefix) = @_;
    my @statuses;
    my $status_txt_file = "$base_name.txt";
    my $status_text = file_slurp ($status_txt_file);
    while ($status_text =~ 
       m/
        \%status\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\n
        \%meaning\s*(.*?)\s*\n\%\s*\n
        /gxs) {
    my ($code, $meaning) = ($1, $2);
    $code = $prefix."_$code";
    $meaning =~ s/\s+/ /g;
    push @statuses, [$code, $meaning];
    }
    return @statuses;
}

читает этот файл и записывает заголовочный файл:

typedef enum kinopiko_status {
    kinopiko_status_ok,
    kinopiko_status_eof_reading_content,

и файл C:

/* Generated by ./kinopiko-status.pl at 2009-11-09 23:45. */
#include "kinopiko-status.h"
const char * kinopiko_status_strings[26] = {
"The routine completed its work successfully.",
"The routine encountered the end of the input before it expected to. ",

используя входной файл вверху. Здесь также рассчитывается число 26 путем подсчета входных строк. (На самом деле существует двадцать шесть возможных статусов.)

Затем создание файла строки состояния автоматизируется с помощью make.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...