Reputation: 26060
I'm writing a C++ abstraction for a C library. The C library has several typedefs for IDs that identify remote resources:
typedef int color_id;
typedef int smell_id;
typedef int flavor_id;
// ...
color_id createColor( connection* );
void destroyColor( connection*, color_id );
// ...
All these typedefs are of course the same type in the compiler's eyes. This is a problem for me, because I would like to overload functions and specialize templates to offer a nice C++ friendly API:
// can't do the following since `color_id`, `smell_id` and `int` are the same
std::ostream& operator<<( std::ostream&, color_id );
std::ostream& operator<<( std::ostream&, smell_id );
void destroy( connection*, color_id );
void destroy( connection*, smell_id );
// no static check can prevent the following
smell_id smell = createSmell( connection );
destroyColor( connection, smell ); // it's a smell, not a color!
Since I don't know of any other way, I've been thinking about creating a different wrapper type for each C type. But this path seems quite rough...
There's a lot of code out there already specialized for primitive types (e.g. std::hash
).
Is there any way to tell the compiler something like "if something has a specialization for int
, but not for my wrapper, then just use the int
specialization"?
Should I otherwise write specializations for stuff like std::hash
? What about similar templated structs that are not in std
(e.g. stuff in boost, Qt etc)?
Should I go with implicit or explicit constructor and casting operator? Explicit ones are of course safer, but they would make it very tedious to interact with existing code and third party libraries that use the C API.
I'm more than open to any tips from whoever has already been there!
Upvotes: 5
Views: 229
Reputation: 10740
Your best bet would be to create a wrapper class, but using templates we can write one wrapper class template and use it for all the different IDs just by assigning them to different instances of the template.
template<class ID>
struct ID_wrapper
{
constexpr static auto name() -> decltype(ID::name()) {
return ID::name();
}
int value;
// Implicitly convertible to `int`, for C operability
operator int() const {
return value;
}
};
std::hash
(just once)We can stick whatever traits we want in the ID
class, but I provided name()
as an example. Since ID_Wrapper
is written as a template, specializing it for std::hash
and other classes only has to be done once:
template<class ID>
class std::hash<ID_wrapper<ID>> : public std::hash<int>
{
public:
// I prefer using Base to typing out the actual base
using Base = std::hash<int>;
// Provide argument_type and result_type
using argument_type = int;
using result_type = std::size_t;
// Use the base class's constructor and function call operator
using Base::Base;
using Base::operator();
};
If you want, we could also specialize operator<<
, but ID_wrapper
is implicitly convertible to an int
anyways:
template<class ID>
std::ostream& operator<<(std::ostream& stream, ID_Wrapper<ID> id) {
stream << '(' << ID_Wrapper<ID>::name() << ": " << id.value << ')';
return stream;
}
Once we have that, we just write a traits class for each ID type!
struct ColorIDTraits {
constexpr static const char* name() {
return "color_id";
}
};
struct SmellIDTraits {
constexpr static const char* name() {
return "smell_id";
}
};
struct FlavorIDTraits {
constexpr static const char* name() {
return "flavor_id";
}
};
We can then typedef
the ID_wrapper:
using color_id = ID_wrapper<ColorIDTraits>;
using smell_id = ID_wrapper<SmellIDTraits>;
using flavor_id = ID_wrapper<FlavorIDTraits>;
Upvotes: 3