Alternative to expanding templates in a switch sta

2019-05-14 11:53发布

问题:

I am using this type of pattern in my code to handle traits for various things. Firstly, I have a set of traits templates; these are specialised by an enum value:

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;
};

I then have code which uses the traits. Mostly, it uses them directly, but in some cases it needs to switch on the enum value, for example:

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;
}

This is for run-time rather than compile-time introspection. My problem is that it requires every single enum to be cased in the switch statement, which is a pain to maintain. I now have a situation where I need to handle all the combinations of two sets of enums, and this would require nested switch statements if I was to handle it as above. The combinatorial complexity obviously won't scale, but at the same time I can't see a good way to drive the template expansion for each combination other than explicitly. This is a contrived example of using such combinatorial expansion for run-time unit conversion:

#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";
}

In other situations, I'm using boost::variant and its static_visitor to drive expansion of all the combinations. This works well, but it might not work here--the types for different traits may be the same but have different behaviour. Unless it's possible to encode the enum values in the variant type?

Or would the Boost Preprocessor macros or C++11 variadic templates be able to provide a better solution here? Edit: Or maybe boost::mpl::foreach?

Thanks for any suggestions, Roger

回答1:

Boost.Preprocessor would allow you to hide the combinatorial complexity.

To use it, you would move the "master" definition of the type list into a Boost.Preprocessor data type. I like sequences, so I will use one:

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

This would then be used to generate the enumeration:

enum class PixelType
{
  BOOST_PP_SEQ_ENUM(TYPES_SUPPORTED)
};

And to wrap a switch statement:

#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

Or wrap a "quadratic" switch statement:

#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

In the code above, ma is used as a prefix for macro arguments. Also, %% is used to indicate the value is not used. Both are just my personal convention.

The macros ONE_CASE, TOPLEVEL_CASE, and NESTED_CASE are your code—basically subroutines of the "function" BOOST_PP_SEQ_FOR_EACH. Inside them, maType (or maToplevelType and maNestedType) will refer to the compile-time indetifiers of the enumerators currently "chosen."



回答2:

You can let the compiler generate a lookup table at compile time which stores function pointers for conversion between each enum combination.

At runtime this lookup table is queried and the returned converter function is executed.

The code below implements this concept for your example:

#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

If you omit throwing an exception in case the index is wrong, both clang and gcc manage to inline the function pointer call: see assembly on godbolt