【问题标题】:Alternative to expanding templates in a switch statement在 switch 语句中扩展模板的替代方法
【发布时间】:2016-02-12 11:00:13
【问题描述】:

我在我的代码中使用这种类型的模式来处理各种事物的特征。首先,我有一组特征模板;这些由枚举值专门化:

template<int>
struct PixelProperties;

/// Properties of UINT8 pixels.
template<>
struct PixelProperties< ::ome::xml::model::enums::PixelType::UINT8> :
  public PixelPropertiesBase<PixelProperties< ::ome::xml::model::enums::PixelType::UINT8> >
{
  /// Pixel type (standard language type).
  typedef uint8_t std_type;

  /// Pixel type (big endian).
  typedef boost::endian::big_uint8_t big_type;
  /// Pixel type (little endian).
  typedef boost::endian::little_uint8_t little_type;
  /// Pixel type (native endian).
  typedef boost::endian::native_uint8_t native_type;

  /// This pixel type is not signed.
  static const bool is_signed = false;
  /// This pixel type is integer.
  static const bool is_integer = true;
  /// This pixel type is not complex.
  static const bool is_complex = false;
};

然后我就有了使用这些特征的代码。大多数情况下,它直接使用它们,但在某些情况下它需要打开枚举值,例如:

bool
isComplex(::ome::xml::model::enums::PixelType pixeltype)
{
  bool is_complex = false;

  switch(pixeltype)
    {
    case ::ome::xml::model::enums::PixelType::INT8:
      is_complex = PixelProperties< ::ome::xml::model::enums::PixelType::INT8>::is_complex;
      break;
    case ::ome::xml::model::enums::PixelType::INT16:
      is_complex = PixelProperties< ::ome::xml::model::enums::PixelType::INT16>::is_complex;
      break;
    [...]
    }

  return is_complex;
}

这是用于运行时而非编译时的自省。我的问题是它需要在 switch 语句中对每个枚举进行大小写,这很难维护。我现在有一种情况,我需要处理两组枚举的所有组合,如果我要像上面那样处理它,这将需要嵌套的 switch 语句。组合复杂性显然不会扩展,但与此同时,除了显式之外,我看不到驱动每个组合的模板扩展的好方法。这是使用这种组合扩展进行运行时单位转换的人为示例:

#include <iostream>
#include <boost/units/unit.hpp>
#include <boost/units/make_scaled_unit.hpp>
#include <boost/units/quantity.hpp>
#include <boost/units/systems/si.hpp>

using boost::units::quantity;
using boost::units::quantity_cast;
using boost::units::make_scaled_unit;
using boost::units::scale;
using boost::units::static_rational;
namespace si = boost::units::si;

enum LengthUnit
  {
    MILLIMETRE,
    MICROMETRE,
    NANOMETRE
  };

template<int>
struct UnitProperties;

template<>
struct UnitProperties<MILLIMETRE>
{
  typedef make_scaled_unit<si::length,scale<10,static_rational< -3> > >::type unit_type;
};

template<>
struct UnitProperties<MICROMETRE>
{
  typedef make_scaled_unit<si::length,scale<10,static_rational< -6> > >::type unit_type;
};

template<>
struct UnitProperties<NANOMETRE>
{
  typedef make_scaled_unit<si::length,scale<10,static_rational< -9> > >::type unit_type;
};

struct Quantity
{
  double value;
  LengthUnit unit;
};

template<int SrcUnit, int DestUnit>
double
convert(double value)
{
  typedef typename UnitProperties<SrcUnit>::unit_type src_unit_type;
  typedef typename UnitProperties<DestUnit>::unit_type dest_unit_type;

  quantity<src_unit_type, double> src(quantity<src_unit_type, double>::from_value(value));
  quantity<dest_unit_type, double> dest(src);
  return quantity_cast<double>(dest);
}

Quantity
convert(Quantity q, LengthUnit newunit)
{
  switch(q.unit)
    {
    case MILLIMETRE:
      switch(newunit)
        {
        case MILLIMETRE:
          return Quantity({convert<MILLIMETRE, MILLIMETRE>(q.value), MILLIMETRE});
          break;
        case MICROMETRE:
          return Quantity({convert<MILLIMETRE, MICROMETRE>(q.value), MICROMETRE});
          break;
        case NANOMETRE:
          return Quantity({convert<MILLIMETRE, NANOMETRE>(q.value), NANOMETRE});
          break;
        }
      break;
    case MICROMETRE:
      switch(newunit)
        {
        case MILLIMETRE:
          return Quantity({convert<MICROMETRE, MILLIMETRE>(q.value), MILLIMETRE});
          break;
        case MICROMETRE:
          return Quantity({convert<MICROMETRE, MICROMETRE>(q.value), MICROMETRE});
          break;
        case NANOMETRE:
          return Quantity({convert<MICROMETRE, NANOMETRE>(q.value), NANOMETRE});
          break;
        }
      break;
    case NANOMETRE:
      switch(newunit)
        {
        case MILLIMETRE:
          return Quantity({convert<NANOMETRE, MILLIMETRE>(q.value), MILLIMETRE});
          break;
        case MICROMETRE:
          return Quantity({convert<NANOMETRE, MICROMETRE>(q.value), MICROMETRE});
          break;
        case NANOMETRE:
          return Quantity({convert<NANOMETRE, NANOMETRE>(q.value), NANOMETRE});
          break;
        }
      break;
    }
}

int main()
{
  Quantity q { 34.5, MICROMETRE };

  auto r = convert(q, NANOMETRE);

  std::cout << q.value << " micrometres is " << r.value << " nanometres\n";
}

在其他情况下,我使用 boost::variant 及其 static_visitor 来推动所有组合的扩展。这很好用,但在这里可能行不通——不同特征的类型可能相同,但行为不同。除非可以对变量类型中的枚举值进行编码?

或者 Boost 预处理器宏或 C++11 可变参数模板能够提供更好的解决方案吗?编辑:或者可能是 boost::mpl::foreach?

感谢您的任何建议, 罗杰

【问题讨论】:

  • 你能发一个minimal reproducible example 来展示“两组枚举的组合”吗?
  • 当然可以。我添加了一个简单的示例,以使用具有 3×3 枚举(9 种组合)的嵌套 switch 语句来演示这一点。正是这种情况下,我有兴趣改进可维护性和运行时效率。 (在实践中,我将至少使用 20 种组合,制作约 400 种组合)。

标签: c++ templates boost enums


【解决方案1】:

Boost.Preprocessor 将允许您隐藏组合复杂性。

要使用它,您可以将类型列表的“主”定义移动到 Boost.Preprocessor 数据类型中。我喜欢序列,所以我会使用一个:

#define TYPES_SUPPORTED (UINT8)(INT8)(UINT16)(INT16)

这将用于生成枚举:

enum class PixelType
{
  BOOST_PP_SEQ_ENUM(TYPES_SUPPORTED)
};

并包装 switch 声明:

#define ONE_CASE(maR, maProperty, maType) \
  case maType: \
    maProperty = PixelProperties< ::ome::xml::model::enums::PixelType::maType>::maProperty;\
    break;

switch (pixelType)
{
  BOOST_PP_SEQ_FOR_EACH(ONE_CASE, is_complex, TYPES_SUPPORTED)
}

#undef ONE_CASE

或者包装一个“二次”switch 语句:

#define TOPLEVEL_CASE(maR, maUnused, maType) \
  case maType: \
    switch (type2) { \
      BOOST_PP_SEQ_FOR_EACH_R(maR, NESTED_CASE, maType, TYPES_SUPPORTED) \
    } \
    break;

#define NESTED_CASE(maR, maToplevelType, maNestedType) \
  case maNestedType: /* do whatever you need, with maToplevelType and maNestedType */; break;

switch (type1) {
  BOOST_PP_SEQ_FOR_EACH(TOPLEVEL_CASE, %%, TYPES_SUPPORTED)
}

#undef TOPLEVEL_CASE
#undef NESTED_CASE

在上面的代码中,ma 用作macro arguments 的前缀。此外,%% 用于指示未使用该值。两者都只是我的个人惯例。

ONE_CASETOPLEVEL_CASENESTED_CASE 是您的代码——基本上是“函数”BOOST_PP_SEQ_FOR_EACH 的子例程。在其中,maType(或maToplevelTypemaNestedType)将引用当前“选择”的枚举数的编译时标识符。

【讨论】:

  • 感谢您的建议。我已经尝试根据您的示例代码调整我的示例:paste.debian.net/381246。不幸的是,这不能编译,我想是因为我有一些微妙的错误。诸如“'MILLIMETRE' 不能用作函数”(来自类型列表)和“'TO_CASE' 未在此范围内声明”(它是,除非它包含错误)之类的东西。不过,我有点不确定到底出了什么问题!
  • 请注意,您使用递归的示例不起作用,因为 _R 变体实际上不是可重入的。请参阅此处注意我的编译失败的答案:stackoverflow.com/questions/35753893/… 以获取此问题的解决方案。
【解决方案2】:

您可以让编译器在编译时生成一个查找表,其中存储用于在每个枚举组合之间转换的函数指针。

在运行时查询此查找表并执行返回的转换器函数。

下面的代码为您的示例实现了这个概念:

#include <iostream>
#include <utility>
#include <array>
#include <functional>
#include <stdexcept>

#include <boost/units/unit.hpp>
#include <boost/units/make_scaled_unit.hpp>
#include <boost/units/quantity.hpp>
#include <boost/units/systems/si.hpp>

using boost::units::make_scaled_unit;
using boost::units::scale;
using boost::units::static_rational;
namespace si = boost::units::si;


struct LengthUnit
{
    enum class Enum
    {
        MILLIMETRE,
        MICROMETRE,
        NANOMETRE
    };

    // this allows safe iteration over all possible enum values
    static constexpr std::array<Enum,3> all = {Enum::MILLIMETRE, Enum::MICROMETRE, Enum::NANOMETRE};
};


struct Quantity
{
  double value;
  LengthUnit::Enum unit;
};


template<LengthUnit::Enum>
struct UnitProperties;

template<>
struct UnitProperties<LengthUnit::Enum::MILLIMETRE>
{
  typedef make_scaled_unit<si::length,scale<10,static_rational< -3> > >::type unit_type;
};

template<>
struct UnitProperties<LengthUnit::Enum::MICROMETRE>
{
  typedef make_scaled_unit<si::length,scale<10,static_rational< -6> > >::type unit_type;
};

template<>
struct UnitProperties<LengthUnit::Enum::NANOMETRE>
{
  typedef make_scaled_unit<si::length,scale<10,static_rational< -9> > >::type unit_type;
};



template <typename EnumHolder>
struct Converter;

// specialization for LengthUnit 
template <>
struct Converter<LengthUnit>
{

using FunctionPtr = double(*)(double);

template <LengthUnit::Enum SrcUnit, LengthUnit::Enum DestUnit>
static double convert(double value)
{
  typedef typename UnitProperties<SrcUnit>::unit_type src_unit_type;
  typedef typename UnitProperties<DestUnit>::unit_type dest_unit_type;

  using boost::units::quantity;
  using boost::units::quantity_cast;

  quantity<src_unit_type, double> src(quantity<src_unit_type, double>::from_value(value));
  quantity<dest_unit_type, double> dest(src);
  return quantity_cast<double>(dest);
}
};

template <typename EnumHolder, typename FunctionPtr, std::size_t... Is>
constexpr auto make_lookup_table_impl(std::index_sequence<Is...>) -> std::array<FunctionPtr, sizeof...(Is)>
{
    constexpr std::size_t size = EnumHolder::all.size();
    return { Converter<EnumHolder>::template convert<EnumHolder::all[Is/size], EnumHolder::all[Is%size]>... };
}

template <typename EnumHolder>
constexpr auto make_lookup_table()
{
    constexpr std::size_t size = EnumHolder::all.size();
    using FunctionPtr = typename Converter<EnumHolder>::FunctionPtr;
    return make_lookup_table_impl<EnumHolder, FunctionPtr>(std::make_index_sequence<size*size>{});
}

template <typename EnumHolder, typename... Args>
auto convert(typename EnumHolder::Enum e1, typename EnumHolder::Enum e2, Args&... args)
{
    static constexpr auto table = make_lookup_table<EnumHolder>();
    static constexpr std::size_t size = EnumHolder::all.size();

    using utype = typename std::underlying_type<typename EnumHolder::Enum>::type;
    const std::size_t index = static_cast<utype>(e1)*size + static_cast<utype>(e2);

    if (index >= size*size)
    {
        throw std::invalid_argument("combination of enum values is not valid");
    }
    return table[index](std::forward<Args>(args)...);
}

int main()
{
  Quantity q { 34.5, LengthUnit::Enum::MICROMETRE };

  auto new_value = convert<LengthUnit>(q.unit, LengthUnit::Enum::NANOMETRE, q.value);

  Quantity r{new_value, LengthUnit::Enum::NANOMETRE};

  std::cout << q.value << " micrometres is " << r.value << " nanometres\n";
}

live example

如果在索引错误的情况下省略抛出异常,clang 和 gcc 都会设法内联函数指针调用:see assembly on godbolt

【讨论】:

  • 感谢您的示例。我对其进行了修改以添加一个额外的函数:Quantity plain_convert(Quantity q, LengthUnit::Enum e) { return Quantity{convert&lt;LengthUnit&gt;(q.unit, e, q.value), e}; } and then updated the main function to call it: Quantity r = plain_convert(q, LengthUnit::Enum::NANOMETRE);`,针对不同的转换组合重复。这是为了确认它确实在编译时针对所有变体进行了扩展,并且确实看起来确实按预期工作。
  • 虽然我想在这个项目中使用 C++11,但目前还不允许,所以我必须在此期间尝试使用预处理器解决方案,但我肯定会寻找长期使用此解决方案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-01-19
  • 2018-01-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-11-22
相关资源
最近更新 更多