I often run into situations (in my C++/C++11 code), where I have a type that basically behaves like a built-in type (or a "basic simple" type like std::string
), but that has a meaning
beyond a 32 bit number or a bunch of characters.
I didn't find anything useful on the Internet, because I don't really what terms to search for...
Examples:
std::string
s (probably not the best idea in the first place, but that's a different story). What was really bad though was the fact, that these IDs were passed through the system as std::string
s or as const char*
s. So it was hard (impossible) to tell where in the
code base IDs were used when searching for the type. The variable names were all variations of ID(ID, id, Id) or key or just i or name or whatever. So you could not search by name either. So I'd prefer to pass those variables as type id_t
.uint16_t
s. But I would like to pass them as network_port_t
s.I generally used typedefs to make things a little nicer. This approach has multiple problems though:
std::string
instead of id_t
).Another thing I tried with the network port example was writing a thin wrapper class sporting a operator uint16_t
. This solved the problem with forward declarations. But then I ran
into a trap with some logging macros which used printf internally. The printfs still worked (well, compiled), but didn't print the port number, but (I think) the address of the object.
I figured with dimensions like weights or lengths Boost.Units might be worth a look (even so it appears a little "heavy"). But for the two examples above, it doesn't fit.
What is the best practice to achieve what I want (using Boost is an option)?
In short: What I want to achieve is to pass "types with higher meaning" as its own type and not as the plain raw/low level/non-abstract type. (Kind of) like having a user defined type. Preferably without the huge overhead of writing a complete class for every type with basically identical implementations, only to be able to do what built-ins already can do.
The _t implies a typedef, a defined data type. The typedef is based on an existing type. Here are the basic C language data types: char. int.
A long on some systems is 32 bits (same as an integer), the int64_t is defined as a 64 bit integer on all systems (otherwise known as a long long).
You can use BOOST_STRONG_TYPEDEF to get some convenience.
It does employ macros, and I think you get to do heterogeneous comparisons (e.g. id == "123"
).
There's two versions, be sure to take the one from Boost Utility.
For strings you can cheat the system by using flavoured strings (inventor: R.Martinho Fernandes).
This leverages the fact that you can actually vary the traits on a std::basic_string
, and create actually different tagged aliases:
#include <string>
#include <iostream>
namespace dessert {
template <typename Tag>
struct not_quite_the_same_traits : std::char_traits<char> {};
template <typename Tag>
using strong_string_alias = std::basic_string<char, not_quite_the_same_traits<Tag>>;
using vanilla_string = std::string;
using strawberry_string = strong_string_alias<struct strawberry>;
using caramel_string = strong_string_alias<struct caramel>;
using chocolate_string = strong_string_alias<struct chocolate>;
template <typename T>
struct special;
template <typename T>
using special_string = strong_string_alias<special<T>>;
std::ostream& operator<<(std::ostream& os, vanilla_string const& s) {
return os << "vanilla: " << s.data();
}
std::ostream& operator<<(std::ostream& os, strawberry_string const& s) {
return os << "strawberry: " << s.data();
}
std::ostream& operator<<(std::ostream& os, caramel_string const& s) {
return os << "caramel: " << s.data();
}
std::ostream& operator<<(std::ostream& os, chocolate_string const& s) {
return os << "chocolate: " << s.data();
}
template <typename T>
std::ostream& operator<<(std::ostream& os, special_string<T> const& s) {
return os << "special: " << s.data();
}
}
int main() {
dessert::vanilla_string vanilla = "foo";
dessert::strawberry_string strawberry = "foo";
dessert::caramel_string caramel = "foo";
dessert::chocolate_string chocolate = "foo";
std::cout << vanilla << '\n';
std::cout << strawberry << '\n';
std::cout << caramel << '\n';
std::cout << chocolate << '\n';
dessert::special_string<struct nuts> nuts = "foo";
std::cout << nuts << '\n';
}
To create an integer that's not an integer (or a string that's not a string) and cannot promote or demote to it), you can only create a new type, that merely means "write a new class". There is no way -at least on basic type- to inherit behaviour without aliasing. A new_type<int>
has no arithmetic (unless you'll define it).
But you can define a
template<class Innertype, class Tag>
class new_type
{
Innertype m;
public:
template<class... A>
explicit new_type(A&&... a) :m(std::forward<A>(a)...) {}
const Innertype& as_native() const { return m; }
};
and do all the workout only once for all.
template<class T, class I>
auto make_new_type(I&& i)
{ return new_type<I,T>(std::forward<I>(i)); }
template<class A, class B, class T>
auto operator+(const new_type<A,T>& a, const new_type<B,T>& b)
{ return make_new_type<T>(a.as_native()+b.as_native()); }
....
and then
struct ID_tag;
typedef new_type<std::string,ID_tag> ID;
struct OtehrID_tag;
typedef new_type<std::string,OtehrID_tag> OtherID;
and ID
oand OtherID
cannot mix in expressions.
NOTE:
auto -function with unspecifyed return are standard from C++14, but GCC accepts it in C++11 as-well.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With