Является ли этот шаблон одобренным для обратной связи с исходным кодом перехода из С++ 03 enum в класс переименования С++ 11?

Мы собираемся перенести (в течение следующих двух лет) все наши компиляторы в компиляторы С++ 11-ready.

Наши клиенты будут использовать наши заголовки, и теперь мы можем писать (более или менее с нуля) заголовки для нашего нового API.

Итак, мы должны выбирать между сохранением списков С++ 03 (со всеми их бородавками) или использовать класс упаковки для имитации нотации С++ 11, потому что мы хотим, в конце концов, переместить эти перечисления на С++ 11.

Идиома "LikeEnum", предложенная ниже жизнеспособного решения, или неожиданные сюрпризы скрываются за ней?

template<typename def, typename inner = typename def::type>
class like_enum : public def
{
  typedef inner type;
  inner val;

public:

  like_enum() {}
  like_enum(type v) : val(v) {}
  operator type () const { return val; }

  friend bool operator == (const like_enum & lhs, const like_enum & rhs) { return lhs.val == rhs.val; }
  friend bool operator != (const like_enum & lhs, const like_enum & rhs) { return lhs.val != rhs.val; }
  friend bool operator <  (const like_enum & lhs, const like_enum & rhs) { return lhs.val <  rhs.val; }
  friend bool operator <= (const like_enum & lhs, const like_enum & rhs) { return lhs.val <= rhs.val; }
  friend bool operator >  (const like_enum & lhs, const like_enum & rhs) { return lhs.val >  rhs.val; }
  friend bool operator >= (const like_enum & lhs, const like_enum & rhs) { return lhs.val >= rhs.val; }
};

Что позволит нам обновить наши перечисления без необходимости внесения нежелательных изменений в код пользователя:

//    our code (C++03)                   |     our code C++11
// --------------------------------------+---------------------------
                                         |
struct KlingonType                       | enum class Klingon
{                                        | {
   enum type                             |    Qapla,
   {                                     |    Ghobe,
      Qapla,                             |    Highos
      Ghobe,                             | } ;
      Highos                             |
   } ;                                   |
} ;                                      |
                                         |
typedef like_enum<KlingonType> Klingon ; |
                                         |
// --------------------------------------+---------------------------
//                client code (both C++03 and C++11)

void foo(Klingon e)
{
   switch(e)
   {
      case Klingon::Qapla :  /* etc. */ ; break ;
      default :              /* etc. */ ; break ;
   }
}

Примечание. Функция LikeEnum была вдохновлена ​​типом безопасного итеративного перехвата

Примечание 2: Совместимость источника не распространяется на ошибку компиляции из-за неявного преобразования в int: они считаются нежелательными, и клиент будет уведомлен заранее, чтобы сделать явное преобразование целых чисел.

Ответ 1

Короткий ответ - да, это жизнеспособное решение (с одним исправлением).

Вот длинный ответ.:)


У вас есть ошибка времени компиляции с вашими функциями сравнения, строго говоря. Это вызовет проблемы с переносимостью со стандартными компиляторами. В частности, рассмотрим следующее:

bool foo(Klingon e) { return e == Klingon::Qapla }

Компилятор не должен знать, какую перегрузку использовать operator==, как неявно (operator type() const), так и преобразование e в KlingonType::type и неявное преобразование Klingon::Qapla в Klingon (через Klingon(type)) требуется одно преобразование.

Требование operator type() const быть explicit исправляет эту ошибку. Конечно, explicit не существует в С++ 03. Это означает, что вам нужно будет сделать, как @Yakk предлагает в комментариях и использовать что-то похожее на idiom safe-bool для типа inner. Удаление operator type() const полностью не является вариантом, потому что оно будет удалять явные преобразования в интегральные типы.

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

friend bool operator == (const like_enum & lhs, const like_enum & rhs) { return lhs.val == rhs.val; }
friend bool operator != (const like_enum & lhs, const like_enum & rhs) { return lhs.val != rhs.val; }
friend bool operator <  (const like_enum & lhs, const like_enum & rhs) { return lhs.val <  rhs.val; }
friend bool operator <= (const like_enum & lhs, const like_enum & rhs) { return lhs.val <= rhs.val; }
friend bool operator >  (const like_enum & lhs, const like_enum & rhs) { return lhs.val >  rhs.val; }
friend bool operator >= (const like_enum & lhs, const like_enum & rhs) { return lhs.val >= rhs.val; }

вам также понадобится:

friend bool operator ==(const like_enum& lhs, const type rhs) { return lhs.val == rhs; }
friend bool operator !=(const like_enum& lhs, const type rhs) { return lhs.val != rhs; }
friend bool operator < (const like_enum& lhs, const type rhs) { return lhs.val <  rhs; }
friend bool operator <=(const like_enum& lhs, const type rhs) { return lhs.val <= rhs; }
friend bool operator > (const like_enum& lhs, const type rhs) { return lhs.val >  rhs; }
friend bool operator >=(const like_enum& lhs, const type rhs) { return lhs.val >= rhs; }
friend bool operator ==(const type lhs, const like_enum& rhs) { return operator==(rhs, lhs); }
friend bool operator !=(const type lhs, const like_enum& rhs) { return operator!=(rhs, lhs); }
friend bool operator < (const type lhs, const like_enum& rhs) { return operator> (rhs, lhs); }
friend bool operator <=(const type lhs, const like_enum& rhs) { return operator>=(rhs, lhs); }
friend bool operator > (const type lhs, const like_enum& rhs) { return operator< (rhs, lhs); }
friend bool operator >=(const type lhs, const like_enum& rhs) { return operator<=(rhs, lhs); }

После исправления вышеизложенного существует незначительная заметная разница семантически (игнорирование невозможных преобразований). Единственное различие, которое я нашел, это значение std::is_pod<Klingon>::value из <type_traits> в С++ 11. Используя версию С++ 03, это будет false, тогда как при использовании enum class es это будет true. На практике это означает (без оптимизации), что a Klingon с использованием enum class может переноситься в регистре, тогда как версия like_enum должна быть в стеке.

Поскольку вы не укажете базовое представление enum class, sizeof(Klingon), вероятно, будет одинаковым для обоих, но я бы не стал полагаться на него. Ненадежность базового представления, выбранного разными реализациями, была частью мотивации строго типизированного enum в конце концов.

Здесь доказательство вышеупомянутых двух абзацев для clang++ 3.0+, g++ 4.5+ и msvc 11 +.


Теперь с точки зрения скомпилированного вывода оба явно будут иметь несовместимый ABI. Это означает, что вся ваша кодовая база должна использовать либо одну, либо другую. Они не будут смешиваться. Для моей системы (clang++ - 3.5 в OSX) приведенный выше символ функции __Z1f9like_enumI11KlingonTypeNS0_4typeEE для версии С++ 03 и __Z1f7Klingon для версии С++ 11. Это должно быть проблемой только в том случае, если они экспортируют библиотечные функции.

Выбранная сборка идентична в моем тестировании для clang++ и g++ после поворота оптимизаций на -O2. Предположительно другие оптимизирующие компиляторы также смогут развернуть Klingon до KlingonType::type. Без оптимизаций версия enum class все равно будет, конечно, избегать всех вызовов функции конструктора и оператора сравнения.