Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How best to implement the "newtype" idiom in C++? [duplicate]

Tags:

c++

newtype

Since learning Rust, I've become a fan of the newtype idiom which I gather Rust borrowed from Haskell.

A newtype is a distinct type based on a standard type which ensures that function parameters are of the correct type.
For example, the old_enough function below must be passed an age in Years. It will not compile with an age in Days or as a plain i64.

struct Days(i64);
struct Years(i64);

fn old_enough(age: &Years) -> bool {
    age.0 >= 18
}

This is different from a typedef or using declaration in C++ which simply renames the type.
For example the old_enough function below would accept an int, an age in Days, or anything else that converts to an int:

typedef int Days;
using Years = int;

bool old_enough(Years age) {
    return age >= 18;
}

As the example above just uses integers, this post on Reddit suggests using enum classes, e.g.:

enum class Days : int {};
enum class Years : int {};

bool old_enough(Years age) {
    return static_cast<int>(age) >= 18;
}

Or it could simply use structures, like Rust e.g.:

struct Days final {int value;};
struct Years final {int value;};

bool old_enough(Years age) {
    return age.value >= 18;
}

What is the best way to implement the newtype idiom in C++?
Is there a standard method?

EDIT the question Strongly typed using and typedef is similar. However, it does not consider the newtype idiom.

like image 404
kenba Avatar asked Jun 26 '20 07:06

kenba


3 Answers

What is the best way to implement the newtype idiom in C++?

Rating the best many times end up in the preferential domain, but you're already mentioned two alternative approaches yourself: simply custom structs wrapping a value of a common type (say int), or using enum classes with an explicitly specified underlying type for strongly type near-identical types.

If you're mainly after strongly typed type aliases of a common type, say

struct Number { int value; }

or, a common type with a parameterizable underlying type

template<typename ValueType = int>
struct Number { ValueType value; }

then another common approach (which also facilitates re-using functionality between strongly type-distinct but related types) is making(/expanding) the Number class (template) a class template parameterized over type template tag parameter, such that specializations over the tag types results in strong typing. As pointed out by @Matthieu M., we may declare a struct as part of the template argument list to a given specialization, allowing a lightweight tag declaration and alias tagging in a single alias declaration:

template<typename Tag, typename ValueType = int>
struct Number {
    ValueType value;
    // ... common number functionality.
};

using YearNumber = Number<struct NumberTag>;
using DayNumber = Number<struct DayTag>;

void takeYears(const YearNumber&) {}
void takeDays(const DayNumber&) {}

int main() {
    YearNumber y{2020};
    DayNumber d{5};
    
    takeYears(y);
    //takeDays(y);  // error: candidate function not viable
    
    takeDays(d);
    //takeYears(d);  // error: candidate function not viable
    
    return 0;
}

Note that in case you would like to specialize non-member functions of the Number class template for specific tags (or e.g. use tag dispatch for a similar purpose), you would need to declare the type tags outside of the alias declaration.

like image 177
dfrib Avatar answered Nov 19 '22 11:11

dfrib


I have used boost strong typedef in the past. The documentation on it seems quite sparse, but fwiw, it seems to be used by facebook, and LLVM seems to have a similar thing called LLVM_YAML_STRONG_TYPEDEF, indicating that it may have had some real-world-exposure.

like image 22
phimuemue Avatar answered Nov 19 '22 12:11

phimuemue


If you have boost, BOOST_STRONG_TYPEDEF does exactly what you want as already seen in this answer.


There is nothing in the c++ language (yet) that can do it directly as you want. But then again, detailed needs could be different, eg. someone might say it's ok to do an implicit construction where as another might say it has to be explicit. Due to that and other combinations1 it is difficult to provide one mechanism that will satisfy everyone and we already have normal type alias (ie. using, which ofc. is different from a strong typedef).

That being said, c++ gives you enough tools that you can build this generic tool yourself and it is not completely difficult to do if you have some experience with templates, etc..

In the end it depends on what newtype issues you actually have, eg. do you just need a handfull or are you going to make these in bulks. For something ordinary like Years and Days you could just use bare structs:

struct Days {int value;};

struct Years {int value;};

However, if you must avoid a situation like this:

bool isold(Years y);

...

isold({5});

You then must make a constructor and make it explicit, ie.:

struct Years {
   explicit Years(int i);
...

1 another combination could for example be if the new type should be allowed to convert to the underlying type, could be usefull for something like int, or it could be dangerous depending on context

like image 2
darune Avatar answered Nov 19 '22 10:11

darune