简体   繁体   中英

How can I elegantly write class templates that use a value implied by the template argument?

I am writing a source file library with a series of declarations and macros that are related in a one-to-one relationship. The first is a list of categories, as an enumeration:

typedef enum {
    CID_SYS,                // Highest-priority devices. Reserved.
    CID_CTRL,               // Controlling unit, only one per segment
    CID_SENSOR,             // Data providers (temperature, speed, clock)
    CID_BROADCAST,          // Broadcast messages (system messages extension)
    ...
} category_id_t;

I'm using this enumeration to define 16-bit message identifiers with the category bits as the most significant 3 bits. Those identifiers are segmented in two variable-size bit blocks in the less significant bits. One of these blocks depends on the above category. So I've also defined a list of sizes as macros, one per category like this:

#define SYS_MESSAGES_MAX        256
#define CTRL_MESSAGES_MAX       64
#define SENSOR_MESSAGES_MAX     8
#define BROADCAST_MESSAGES_MAX  64
...

It's then easy to mask out the category and retrieve the relevant bits, ie the function ID, which lies in the least significant bits of the message ID. With CID_SYS for instance:

unsigned short function_id = message_id & (SYS_MESSAGES_MAX-1)

I need a class template with the category as an argument. The number of messages in the category, which is implied by the latter should somehow be deduced by the template class at compile-time without resorting to arrays. The class template might look like something similar:

template <category_id_t CAT>
class Manager
{
    ...
    unsigned message_count() const { return /* a number depending on CAT */ }
};

With -Os the compiler resolves as much as it can at compile-time without adding code or variables when it can. So I'd like to make the most of it. My current attempt is using a function template and specialization:

template<category_id_t CAT>
unsigned cat_size();

template<category_id_t CAT>
class Manager
{
public:
        unsigned size() const { return cat_size<CAT>(); }
};

template<> unsigned cat_size<CID_SYS>() { return SYS_MESSAGES_MAX; }
template<> unsigned cat_size<CID_CTRL>() { return CTRL_MESSAGES_MAX; }

The above example would then be:

unsigned short function_id = message_id & (size()-1) /* This should be a constant resolved at compile-time */

The generic template function is intentionally left without its definition to have a linker error generated in the case I forget a specialization when I add a category. However I find this inelegant and convoluted.

How could I make this more elegant?

I definitely don't want to pass the message count as a template argument because I still need the C-style macros: my library is supposed to be used by C and C++ applications.

This may not be neat or elegant but I had some search meta-function to find a type in a type-list as follows:

#include <type_traits>

template<typename ...Ts>
struct TypeList; //Represent a list of types to be queried

struct Nil; //empty type, a placeholder type if we cannot find what we need

//Searches given 'Item' in types ('Ts...') where equality check is done by 'Equals'
template<typename Item, template<class,class> class Equals, typename ...Ts>
struct Find;

//Specializes the 'Find' with 'TypeList' provides syntactic sugar
template<typename Item, template<class,class> class Equals, typename ...Ts>
struct Find<Item, Equals, TypeList<Ts...>> : Find<Item, Equals, Ts...> 
{};

//recursive 'Find' metafunction. If 'T' is equal to 'Item' then return 'T'
//                               Else recurse to the rest of the type list
template<typename Item, template<class,class> class Equals, typename T, typename ...Ts>
struct Find<Item, Equals, T, Ts...> {
    using type = typename std::conditional<
        Equals<Item, T>::value, //Evaluate T
        T, //if predicate returns true than T is the type we are looking for
        Find<Item, Equals, Ts...> //else recurse into the list
    >::type;
};

//specialization for one type 'T', that is the last element of the original type-list
template<typename Item, template<class,class> class Equals, typename T>
struct Find<Item, Equals, T> {
    using type = typename std::conditional<
        Equals<Item, T>::value, //Evaluate T
        T, //if predicate returns true than T is the type we are looking for
        Nil //else return Nil for meaningful compile messages
    >::type;
};

You can have this in a utility header and use it for various purposes. Boost has two different libraries for such classes (meta-programming) one of them is MPL and the other modern version is Hana . You may want to check one of those libraries.

With such type-searching mechanism we can define a type for your category and hold category related information.

//A special structure to define your compile-time attributes for each category
template<category_id_t CatId, int CatMask>
struct Category
{
    static const category_id_t id = CatId;
    static const int mask = CatMask;
};

//define a set of categories with their attributes (ie. id and mask)
using Categories = TypeList<
    Category<CID_SYS, SYS_MESSAGES_MAX-1>,
    Category<CID_CTRL, CTRL_MESSAGES_MAX-1>,
    Category<CID_SENSOR, SENSOR_MESSAGES_MAX-1>,
    Category<CID_BROADCAST, BROADCAST_MESSAGES_MAX-1>
>;

Then we define a predicate and a specialized search function to find related category with given id as follows:

//
template<typename Item, typename Category_>
using CategoryEquals = std::integral_constant<
    bool,
    Item::value == Category_::id
>;

template<category_id_t CatId>
using FindCategory = Find<
    std::integral_constant<category_id_t, CatId>, //Item
    CategoryEquals, //Equals
    Categories
>;

Finally, we can find and use categories like this:

template<category_id_t CatId>
unsigned short GetFunctionId(unsigned short messageId)
{
    using FoundCat = typename FindCategory<CatId>::type; //search for category

    return messageId & FoundCat::mask;
}

Sample usage:

int main()
{
    unsigned short msg = 259;
    unsigned short functionId = GetFunctionId<CID_SYS>(msg);

    std::cout << functionId; //prints 3
}

This is actually doable without templates now that we have constexpr. The cat_size mapping can be done using a constexpr function with a switch . You could define *_MESSAGES_MAX as separate constexpr int s if you don't like have the values in the return like the below

constexpr int cat_size(category_id_t cat) {
  switch (cat) {
    case CID_SYS:
      return 256; // SYS_MESSAGES_MAX
    case CID_CTRL:
      return 64;  // CTRL_MESSAGES_MAX
    case CID_SENSOR:
      return 8;   // SENSOR_MESSAGES_MAX
    case CID_BROADCAST:
      return 64;  // BROADCAST_MESSAGES_MAX
  }
}

Calculating the function id is just another constexpr that calls through to the switch. I've replaced unsigned short with std::uint16_t to make sure you get what you want, note this requires you to #include <cstdint>

constexpr std::uint16_t function_id(category_id_t cat, std::uint16_t msg) {
  return msg & (cat_size(cat)-1);
}

Looking at the generated assembly for the following, we can see that it is actually calculated at compile time

int main() {
  constexpr std::uint16_t msg{0xCDEF};
  constexpr auto fid = function_id(CID_SYS, msg);
  std::cout << fid << '\n';
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM