Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I provide template specializations for typedefs of the same type?

A 3rd party SDK defines several typedefs, e.g.:

typedef unsigned char SDK_BYTE
typedef double SDK_DOUBLE
typedef unsigned char SDK_BOOLEAN

It also defines a variant type SdkVariant:

class SdkVariant
{
public:
    enum SdkType { SdkByte, SdkDouble, SdkBoolean };
    bool toByte(SDK_BYTE&);
    bool toDouble(SDK_DOUBLE&);
    bool toBool(SDK_BOOLEAN&);
    SdkType type();
};

Retrieving a value from such a variant looks like this (presuming, we know the type of the contained value):

SdkVariant variant(foobar());
double value;
bool res = variant.toDouble(value);
if (!res)
    diePainfully();
else
    doSomethingWith(value);

This is quite verbose and therefore I want to provide a variant_cast-function-class that can perform the value retrieval and error handling:

// general interface:
template<class T>
class variant_cast
{
public:
    T operator()(const SdkVariant& variant);
};

// template specializations:
template<>
SDK_DOUBLE variant_cast<SDK_DOUBLE>::operator()(const SdkVariant& variant)
{
    SDK_DOUBLE value;
    bool res = variant.toDouble(value);
    if (!res)
        diePainfully();
    return value;
}

template<>
SDK_BYTE variant_cast<SDK_BYTE>::operator()(const SdkVariant& variant)
{
    SDK_BYTE value;
    bool res = variant.toByte(value);
    if (!res)
        diePainfully();
    return value;
}

template<>
SDK_BOOLEAN variant_cast<SDK_BOOLEAN>::operator()(const SdkVariant& variant)
{
    SDK_BOOLEAN value;
    bool res = variant.toByte(value);
    if (!res)
        diePainfully();
    return value;
}

This does not compile (C2995: Function template already defined), because SDK_BYTE and SDK_BOOLEAN are the same types (unsigned char). My idea is now to let the preprocessor check that SDK_BYTE and SDK_BOOLEAN are the same and if so, define a single template specialization for both. If they're different, it should use the two separate specializations from above. Like this:

#if SDK_BYTE == SDK_BOOLEAN
template<>
SDK_BYTE variant_cast<SDK_BYTE>::operator()(const SdkVariant& variant)
{
    SDK_BYTE value;
    bool res;
    if (variant.type() == SdkByte)
        res = variant.toByte(value);
    else
        res = variant.toBool(value);
    if (!res)
        diePainfully();
    return value;
}
#else
    // code from above
#endif

The problem with the above code is, that it appears to be impossible for the preprocessor to resolve the two typedefs. Is there a way to compare two typedefs (correctly) during preprocessing? If not, is there a way to hinder the compiler from resolving the typedefs, so that it would accept two different template specializations for SDK_BYTE and SDK_BOOLEAN? If not, I can still provide the single template specialization and use BOOST_STATIC_ASSERT to make the compiler fail if SDK_BYTE and SDK_BOOLEAN are unequal, but is there a better way to solve my problem?

like image 378
Alexander Tobias Bockstaller Avatar asked Feb 19 '23 11:02

Alexander Tobias Bockstaller


2 Answers

If C++11 is an option for you, here is some code illustrating a possible solution using std::enable_if and std::is_same:

#include <iostream>
#include <type_traits>

struct SdkVariant
{
};

typedef int   type1;
typedef float type2;

template <typename T, typename Enable=void>
class variant_cast
{
public:
  /* Default implementation of the converter. This is undefined, but
     you can define it to throw an exception instead. */
  T operator()(const SdkVariant &v);
};

/* Conversion for type1. */
template <typename T>
class variant_cast<T,typename std::enable_if<std::is_same<T,type1>::value>::type>
{
public:
  type1 operator()(const SdkVariant &v)
  {
    return type1 { 0 };
  }
};

/* Conversion for type2, IF type2 != type1. Otherwise this
   specialization will never be used. */
template <typename T>
class variant_cast<T,typename std::enable_if<
         std::is_same<T,type2>::value
      && !std::is_same<type1,type2>::value>::type>
{
 public:
  type2 operator()(const SdkVariant &v)
  {
    return type2 { 1 };
  }
};

int main()
{
  variant_cast<type1> vc1;
  variant_cast<type2> vc2;
  std::cout << vc1({}) << std::endl;
  std::cout << vc2({}) << std::endl;
  return 0;
}

A few notes:

  1. Instead of the various types you get defined by that library, I have only defined type1 and type2
  2. I have defined an empty SdkVariant struct as a dummy
  3. Because that dummy is empty, my conversion does not really convert anything. It just outputs a constant (value 0) when converting to type1, and a constant (value 1) when converting to type2 (if type2 is actually different from type1).
  4. To test whether it does what you need, you may replace the definition of type2 with

    typedef int type2;
    

    so it is identical with the definition for type1. It will still compile, and there will be no error related to any double definition.

  5. I have tested this using GCC 4.7.0 and the --std=c++11 option.

Remark about the use of std::enable_if and partial vs. explicit template specializations

The converter for type1 is declared as

template <typename T>
variant_cast<T,typename std::enable_if<std::is_same<T,type1>::value>::type>

which means it is defined for any type T that is the same as type1. Instead, we could have used an explicit specialization

template <>
variant_cast<type1>

which is much simpler, and works, too.

The only reason I didn't do that is that in the case of type2 it won't work, because for type2 we must check whether it is the same as type1, i.e. we must use std::enable_if:

template <>
class variant_cast<type2,
   typename std::enable_if<!std::is_same<type1,type2>::value>::type>

Unfortunately, you can't use std::enable_if in an explicit specialization, because an explicit specialization is not a template – it's a real data type, and the compiler must process it. If type1 and type2 are identical, this:

typename std::enable_if<!std::is_same<type1,type2>::value>::type

does not exist, because of the way std::enable_if works. So the compilation fails because it can't instantiate this data type.

By defining the converter for any type T that is the same as type2 we avoid the explicit instantiation for type2, hence we don't force the compiler to process it. It will only process the template specialization for type2 if the std::enable_if<...>::type actually exists. Otherwise it will simply ignore it, which is exactly what we want.

Again, in the case of type1 (and for any further type3, type4 etc.) an explicit instantiation will work.

I think it's worthwhile pointing out that defining a template specialization for any type T that is the same as some type type is a trick that is generally applicable whenever you can't use an explicit specialization for formal reasons, so you use a partial specialization, but you really want to bind it to this one type only. For instance, a member template cannot be explicitly instantiated unless its enclosing template is explicitly instantiated, too. Using a combination of std::enable_if and std::is_same probably helps there, too.

like image 64
jogojapan Avatar answered Feb 22 '23 00:02

jogojapan


You could do it like this:

SDK_BYTE asByte(SdkVariant & var)
{
  SDK_BYTE byte;
  bool const ok = var.toByte(byte);
  if (!ok) diePainfully();
  return byte;
}

SDK_DOUBLE asDouble(SdkVariant & var)
{
  SDK_DOUBLE d;
  bool const ok = var.toDouble(d);
  if (!ok) diePainfully();
  return d;
}

SDK_BOOLEAN asBoolean(SdkVariant & var)
{
  SDK_BOOLEAN b;
  bool const ok = var.toBool(b);
  if (!ok) diePainfully();
  return b;
}

static const bool byteAndBooleanAreTheSame = std::is_same<SDK_BYTE, SDK_BOOLEAN>::value;

template <bool b>
struct VariantCastImpl
{
  template <typename T> T cast(SdkVariant & var) const;

  template <> SDK_DOUBLE cast(SdkVariant & var) const { return asDouble(var); }
  template <> SDK_BYTE cast(SdkVariant & var) const { return asByte(var); }
  template <> SDK_BOOLEAN cast(SdkVariant & var) const { return asBoolean(var); }
};

template <>
struct VariantCastImpl<false>
{
  template <typename T> T cast(SdkVariant & var) const;

  template <> SDK_DOUBLE cast(SdkVariant & var) const { return asDouble(var); }
  template <> SDK_BYTE cast(SdkVariant & var) const
  {
    if (var.type() == SdkVariant::SdkByte)
    {
      return asByte(var);
    }
    else if (var.type() == SdkVariant::SdkBoolean)
    {
      return asBoolean(var);
    }
    else
    {
      diePainfully();
      return SDK_BYTE(); // dummy return, I assume diePainfully throws something
    }
  }
};

template <typename T>
T variant_cast(SdkVariant & var)
{
  return VariantCastImpl<!byteAndBooleanAreTheSame>().cast<T>(var);
};
like image 22
MadScientist Avatar answered Feb 22 '23 00:02

MadScientist