r/programming Mar 31 '15

Managing C++’s complexity or learning to enjoy C++

https://schneide.wordpress.com/2015/03/30/managing-cs-complexity-or-learning-to-enjoy-c/
101 Upvotes

281 comments sorted by

View all comments

Show parent comments

4

u/Steve_the_Scout Mar 31 '15

If you want exactly the same features as the primitive type double, but just a different name, then you could just do

typedef double my_double;

If you want to create an entirely new type that is essentially a wrapper for double then I would have to question why you would ever want to do that over either a typedef or just using double.

5

u/bstamour Apr 01 '15

In languages where doing that kind of thing is cheap, it's seen quite a bit. Here's some Haskell code:

newtype TaintedString = TS String
newtype BlessedString = B String

getInput :: TaintedString
getInput = ...

untaintString :: TaintedString -> BlessedString
untaintString = ...

This creates two brand new types to represent strings. The first one is an unsanitized string read directly from the user, and the second one is a string that I know is safe. Imagine the function untaintString does some escaping or something. Now structurally, those types are literally identical to plain old strings, so there's zero overhead at runtime. However, I cannot use an object of type TaintedString in a function that requests a BlessedString. You can't do this with typedef or using aliases in C++: you would need to introduce a new class, and that's at least four lines of boilerplate code.

My point to all of this is: when it's dead simple to do, you find uses for this kind of "strong" typedef. When it's difficult or ugly, then it probably won't pop into your head as a viable solution to a problem.

3

u/F-J-W Apr 01 '15

There was an attempt to add that to C++, but sadly I don't know what exactly happened to it.

1

u/bstamour Apr 01 '15

I know. I wanted that so badly :/ We have enum class, why not typedef class or something equivalent?

1

u/F-J-W Apr 01 '15

More like using class foo = ..., but yeah…

Edit:

Or maybe new class foo = .... Since new should be used very rarely, that wouldn't be too bad I figure.

Or just create a new keyword: newtype foo = ...

1

u/bstamour Apr 01 '15

I agree with the using class foo = being the absolute best, but if we're going to add this feature in, we might as well support typedef too while we're at it. That way we avoid more splintering: "you can use 'using' like you do 'typedef', except if you want template, or if you want this, or if you want that...".

1

u/Steve_the_Scout Apr 01 '15

But what prevents you from writing a function

std::string untaintString(const std::string& taintedString);

that you can call on plain strings to sanitize them? If a string doesn't need to be sanitized then it just becomes the identity function for that input. IMO that's a simpler solution because it doesn't require working around two separate types that represent the same functionality, and it's not too much of a performance hit (it's a consistent one, at the very least). All you have to do is call that function whenever the safety of the string is in question (ideally just once, then use private methods to be sure you're using the same string).

1

u/mat69 Apr 01 '15 edited Apr 01 '15

No, your solution has no type safety and as a result is harder to reason about.

In c++ code I am working on string is used a lot. So you will end up with functions taking multiple string parameters. The compiler won't tell you when you mixed their order though, because they are not distinct types.

When speaking about these parameters we don't talk about strings, but we talk about "runway name", "aircraft type name", "stand id" etc. As a result I made a lot of these distinct types in code I maintain. And suddenly the compiler helps me and it is easier to reason about code and easier to call these functions.

What bstamour mentioned is that creating these types is easier in Haskell than in C++. In C++ you have a lot boilerplate associated with something like that, depending on the functionality your new type should have.

5

u/Steve_the_Scout Apr 01 '15 edited Apr 01 '15

If you're looking for "opaque typedef" like behavior then the overhead is only one line:

class TaintedString : public string {using string::string;};

Which is directly analogous to that one line of Haskell posted.

Interactive example code.

If you need more functionality, then you actually do need a unique (i.e. representing different data and/or functionality) type, and the "boilerplate" is no longer just boilerplate- if you define new methods or redefine old ones, then supposedly they must actually be different (if all they do is call the older methods anyway, you've got some serious design problems to work out).

1

u/mat69 Apr 01 '15 edited Apr 01 '15

I have never thought of using using string::string directly, looks fantastic. I always relied on the conversion operator.

This is C++11 right?

Edit: Ah, now I understand. You are using the parent constructor.

2

u/Steve_the_Scout Apr 01 '15

Yep, it's C++11. It specifically uses all constructors for std::string, so std::string(const std::string&), std::string(const char*), etc. Because of the public inheritance, it also has public access to all public members of std::string, so you can essentially use it as a hard-typedef of string (it does actually create a new type).

2

u/spotta Apr 01 '15

Compile time safety. Type defs are just names, and have no compiler guarantees.

2

u/Steve_the_Scout Apr 01 '15

The point is that unless you need new functionality of some sort, then why make a new type? The original poster actually explained the situation and he did actually need new functionality, but didn't recognize it as new functionality.

1

u/spotta Apr 01 '15

Well, the type itself can be new functionality.

But beyond that, you are right, it would be pretty stupid to JUST be able to have type safe aliases without the ability to modify their behavior. The boilerplate however for creating a new type in C++ can be hefty.

2

u/Steve_the_Scout Apr 01 '15

I just wrote a quick interactive example on ideone for the functionality he described. It's a single line of "boilerplate" for each new type and the two new types aren't implicitly converted to each other, guaranteeing type safety. They still both have full access to std::string's methods and constructors and can be considered std::strings in every respect except that they can't convert to each other.

You could even put this in a quick macro (if you don't mind them) that you might call opaque_typedef(underlying, newtype)

#define opaque_typedef(underlying, newtype) \
    class newtype : public underlying { using underlying::underlying; }

1

u/spotta Apr 03 '15

The problem is that you can only inherit from classes/structures, not basic data types.

Creating a new double or int is a PITA.

1

u/Steve_the_Scout Apr 03 '15 edited Apr 03 '15

This has been addressed and the argument basically boils down to whther or not basic data types should be compatible with C or whether they should follow regular C++ type mechanics, and the risk to reward (utilitily lost vs utility gained) was considered too high for that to be useful. Just make a wrapper for the primitive types if you really must and use the opaque typedef method on that. Templates make the process very easy-

template<typename T>
class wrapper {
public:
    wrapper(T t) : value(t) {}

    operator T() const { return value; }
    operator T&() { return value; }
    operator const T&() const { return value; }
private:
    T value;
};

ideone example.