Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Simple types with higher meaning (C++11)

Tags:

c++

c++11

boost

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:

  • I once worked on a system, where items were identified by an ID. And these IDs were std::strings (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::strings 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.
  • Network ports: They are uint16_ts. But I would like to pass them as network_port_ts.

I generally used typedefs to make things a little nicer. This approach has multiple problems though:

  • You don't have to use the typedef. You can still pass variables around by the "raw" type (e.g. std::string instead of id_t).
  • If the raw type is a template, you are done with forward declaring the typedef (e.g. with a shared_ptr).
  • "Forward declaring" the typedef is a maintenance problem. If the raw type changes, you get to change stuff all over the place.

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.

like image 813
DrP3pp3r Avatar asked Sep 25 '14 12:09

DrP3pp3r


People also ask

What does _t mean in C?

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.

What is int64_t?

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).


2 Answers

1. Strong Typedefs

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.

2. flavoured_string<>

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';
}
like image 176
sehe Avatar answered Oct 16 '22 10:10

sehe


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.

like image 40
Emilio Garavaglia Avatar answered Oct 16 '22 12:10

Emilio Garavaglia