问题
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?
回答1:
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:
- Instead of the various types you get defined by that library, I have only defined
type1
andtype2
- I have defined an empty
SdkVariant
struct as a dummy - 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 totype2
(iftype2
is actually different fromtype1
). To test whether it does what you need, you may replace the definition of
type2
withtypedef 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.- 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.
回答2:
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);
};
回答3:
Just to be complete, using the BOOST_STRONG_TYPEDEF the code could look like this:
BOOST_STRONG_TYPEDEF(SDK_BYTE, mySDK_BYTE)
BOOST_STRONG_TYPEDEF(SDK_BOOLEAN, mySDK_BOOLEAN)
template<>
mySDK_BYTE variant_cast<mySDK_BYTE>::operator()(const SdkVariant& variant)
{
SDK_BYTE value;
bool res = variant.toByte(value);
if (!res)
diePainfully();
return value;
}
template<>
mySDK_BOOLEAN variant_cast<mySDK_BOOLEAN>::operator()(const SdkVariant& variant)
{
SDK_BOOLEAN value;
bool res = variant.toByte(value);
if (!res)
diePainfully();
return value;
}
This actually works. The only downside is, that for retrieving an SDK_BOOLEAN
or SDK_BYTE
one now has to write variant_cast<mySDK_BOOLEAN>(myVariant)
or variant_cast<mySDK_BYTE>(myVariant)
. Thanks Xeo.
来源:https://stackoverflow.com/questions/11982012/how-can-i-provide-template-specializations-for-typedefs-of-the-same-type