Strong typedefs: a reprise (discussion wanted)

68 views
Skip to first unread message

sval...@gmail.com

unread,
Jun 19, 2018, 1:18:40 PM6/19/18
to ISO C++ Standard - Future Proposals
Around 18 months ago I submitted here a proposal for strong typedefs. I have
received feedback about it, and I've worked on it a bit more. However, by
talking to people at the Rapperswil meeting I have realized that people have
different expectations for strong typedefs as me.

I have some specific questions about strong typedefs features which I'd like to
open for discussion, use-cases, counter-examples, etc. so that we can start to
hone in onto the semantics that strong typedefs should have in C++. If the
discussion is useful, I'd love to come up with additional questions and do this
again until I have a proposal that satisfies all requirements.

Use cases that I know exists are centered on:
- StrongId types: primitive types that have certain operations disabled on
  them.
- GUI applications: integer values with intermediate float math, stop certain
  types of integers from being passed at compile time.
- String manipulation: disabling operations between encrypted and unencrypted
  strings.
- Scientific applications: enable modifying internals without breaking
  interfaces. # I am personally here

That said, here are some questions which I would like you to reflect and
comment upon. I have put my comments there already, but please let me know what
you think. I also don't have direct use-cases for some of these questions, so
if you know of any please let me know.

I'm going to use the placeholder syntax of

struct A = B {};

to indicate a strong typedef from B to A in order to discuss things. I'm also
assuming that strong typedefs and inheritance are mutually exclusive (it is not
possible to do both at the same time).

# (1) SHOULD WE ALLOW STRONG TYPEDEFS OF NON-PRIMITIVE TYPES? #


If non-primitive types are not included, use cases for strong typing existing
classes like strings, vectors, etc are not possible.

If allowed, this would significantly impact the design of the feature, as
non-primitive types can interact in much more complicated ways than primitive
types (see below questions).

# (2) BY DEFAULT SHOULD WE KEEP EVERYTHING THE SAME? #


This seems a simple enough decision to make, but it does have consequences
and if answered affirmatively does create some problems. We'll see them below,
and finally in (9) we discuss a possible alternative.

Most opinions I've gathered on this tend to agree that a simple strong typedef
should be pretty much equivalent to a copy of the original class, so:

struct A {
   
int x;
   
void foo() {}
};

struct B = A {};

/* Equivalent to
struct B {
    int x;
    void foo() {}
}; */

# (3) IS STRONG TYPEDEF (OF STRONG TYPEDEF) OF INHERITING CLASS INHERITING FROM SAME BASES? #

Assuming (1) is answered positively, and given the following:

    struct A {};
   
struct B : A {};
   
struct C = B {};

Does C inherit from A?

This question has consequences in whether it is possible to extend an existing
class without introducing an inheritance dependency (see (4) and (6)).

If a strong typedef of an inheriting class does not inherit from the same
bases, there would be no other way to produce the same effect, unless
additional syntax is introduced, for example:

    struct C = B : A {};

# (4) CAN I ADD ATTRIBUTES TO A CLASS? #

If attributes cannot be added, then there would be no way to extend a class
without the extension being a child of the original. This follows from question
(2), if it is answered in the affirmative. This is bad since it would bypass
most of the usefulness of strong typing.

At the same time, this might bring some problems when trying to build a strong
typedefs. Consider:

    struct A {
       
int x;
        A foo
() { return {1}; }
   
};

   
struct X {
        X
(int) {} // No default constructor
   
};

   
struct B = A {
        B
() : x(1) {}
        X x
;
   
};

    B b
; b.foo(); // ??? we can't convert foo() to construct a B anymore

Note that the implementation of the original A foo() might not be visible to
the writer of B.

# (5) CAN I REMOVE/REDEFINE ATTRIBUTES FROM A CLASS? #

If attributes can be removed/redefined, the functions carried over from the
original class would all break as the layout of the new class has changed.
This might not be obvious to detect as definitions might be in other
translation units. Consider:

    struct A {
       
int x;
       
void foo() { ++x; }
   
};

   
struct B {
       
int x = delete;
   
};

    B b
; b.foo(); // ???

This might be hidden by keeping the deleted members in the class and just
preventing their usage, but it would probably become weird very soon.

Changing access might be doable, although inheritance can already do that.

# (6) CAN I ADD FUNCTIONS TO A CLASS? #

As for attributes, if methods cannot be added, then there is no way to do so
without the extension being a child of the original. Again this follows from
(2), if answered affirmatively.

I haven't found any particular drawbacks here yet though.

# (7) CAN I REMOVE/REDEFINE FUNCTIONS FROM A CLASS? #

Redefinition and removal of member functions would also incur in similar
problems as removal of member attributes, as other member functions might be
using them. This could be partially fixed by hiding removed functions, but
still using them under the hood. However, this still might result in weird
situations. Consider (all in placeholder syntax):

struct A {
   
int x;
    A foo
() { return bar(); }
    A bar
() { return {1}; }
};

struct B = A {
   A
::foo() = default; // Carry foo from A, becomes B foo();
   A
::bar() = delete; // Remove B bar() from interface completely.
};

struct C = B {
    B
::foo() = default; // Carry foo from B, becomes C foo();
};

// User code below
struct D = C {
    C
::foo() = default; // Carry foo from C, becomes D foo();
    D
(int) {}
};

D d
{4};
d
.foo(); // Error, A::bar() cannot construct D anymore, but D user never knew about bar()

This might happen with very deeply nested classes, so that the original
wouldn't really be visible or known to the user. Another problem is how to
detect it at compile time, if the implementation can be in another translation
unit? This might also get compounded if we allowed adding and removing member
attributes.

# (8) SHOULD NON-MEMBER FUNCTIONS CARRY OVER? #

This is one of the hot topics of strong typedefs. Whether there should be a
mechanism to carry over non-member functions to strong typedefs, or not.

While that can be a matter of preference, I just wanted here to make the point
that it is very easy to have something similar to what we have done above no
matter what is chosen:

struct A {
   
int x;
    A foo
();
};

// A.cpp
A bar
() { return {1}; }
A A
::foo() { return bar(); }

// Other files
struct B = A {
   A
::foo() = default; // Carry foo from A, becomes B foo();
   A
::bar() = delete; // Remove B bar() from interface completely.
};
struct C = B {
    B
::foo() = default; // Carry foo from B, becomes C foo();
};

// User code below
struct D = C {
    C
::foo() = default; // Carry foo from C, becomes D foo();
    D
(int) {}
};

D d
{4};
d
.foo(); // What to do about non-member A bar(), invisible from the user?


# (9) BARRING FUNCTIONS THAT RETURN THE ORIGINAL CLASS BY VALUE? #

It has occurred to me that most of these problems only happen when we edit the
original interface (destructively for functions, additively for members) and we
call a member function that should return the class by value.

I haven't found a way to break the editing process in another way, so maybe
simply automatically disabling these functions once member functions are
removed or non-default-constructible attributes are added could suffice. I
haven't explored deeply in this direction yet.


Jens Maurer

unread,
Jun 19, 2018, 1:30:37 PM6/19/18
to std-pr...@isocpp.org
On 06/19/2018 07:18 PM, sval...@gmail.com wrote:
> Around 18 months ago I submitted here a proposal for strong typedefs. I have
> received feedback about it, and I've worked on it a bit more. However, by
> talking to people at the Rapperswil meeting I have realized that people have
> different expectations for strong typedefs as me.
>
> I have some specific questions about strong typedefs features which I'd like to
> open for discussion, use-cases, counter-examples, etc. so that we can start to
> hone in onto the semantics that strong typedefs should have in C++. If the
> discussion is useful, I'd love to come up with additional questions and do this
> again until I have a proposal that satisfies all requirements.
>
> Use cases that I know exists are centered on:
> - StrongId types: primitive types that have certain operations disabled on
> them.

It seems to me this use-case has been addressed by scoped enums with explicit
underlying type, at least for the actual "id" case.

> - GUI applications: integer values with intermediate float math, stop certain
> types of integers from being passed at compile time.
> - String manipulation: disabling operations between encrypted and unencrypted
> strings.
> - Scientific applications: enable modifying internals without breaking
> interfaces. # I am personally here
>
> That said, here are some questions which I would like you to reflect and
> comment upon.

These all seem to apply to strong typedefs for classes.

I'd like to see more (specific) use-cases why those would be useful,
or rather, why derivation and member composition isn't good enough and
what kind of bang-for-the-complexity I'd get with your variant of
strong typedefs.

Jens

Peter Sommerlad

unread,
Jun 19, 2018, 1:44:23 PM6/19/18
to std-pr...@isocpp.org, W Brown
Hi,

With C++17/20 aggregate initialization rules and structured bindings one can create strong types by having the wrapped type as a member. I am playing around with a single header library for exactly just that and can share the incomplete prototype (directly email me, since i might not see the mailing list reply due to my mental bandwidth). I think this might be better a library-only approach than separate syntax. And because of the different use cases, i believe different libraries, even when not standardized, can serve them better than a language solution that fits neither well enough.

There have been other proposals for strong typing, ie inheriting from non-class types, i remember one from Walter Brown that did not make it through evolution working group as far as i remember. 

Regards
Peter

Sent from Peter Sommerlad's iPad
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposal...@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/c7beea3b-69f7-4fc5-9df8-5f667c09049a%40isocpp.org.

Barry Revzin

unread,
Jun 19, 2018, 2:00:42 PM6/19/18
to ISO C++ Standard - Future Proposals, webro...@gmail.com


On Tuesday, June 19, 2018 at 12:44:23 PM UTC-5, PeterSommerlad wrote:
Hi,

With C++17/20 aggregate initialization rules and structured bindings one can create strong types by having the wrapped type as a member. I am playing around with a single header library for exactly just that and can share the incomplete prototype (directly email me, since i might not see the mailing list reply due to my mental bandwidth). I think this might be better a library-only approach than separate syntax. And because of the different use cases, i believe different libraries, even when not standardized, can serve them better than a language solution that fits neither well enough.

There have been other proposals for strong typing, ie inheriting from non-class types, i remember one from Walter Brown that did not make it through evolution working group as far as i remember. 

Regards
Peter


Eugenio Bargiacchi

unread,
Jun 19, 2018, 4:19:41 PM6/19/18
to ISO C++ Standard - Future Proposals
It seems to me this use-case has been addressed by scoped enums with explicit
underlying type, at least for the actual "id" case.

That may be, although they are very clunky for that purpose. You still have to cast every time you want to assign something to them, which is not super-pretty.

These all seem to apply to strong typedefs for classes.

I'd like to see more (specific) use-cases why those would be useful,
or rather, why derivation and member composition isn't good enough and
what kind of bang-for-the-complexity I'd get with your variant of
strong typedefs.

Indeed, strong typedefs for classes would really be useful and so I'm taking them into consideration, but so do previous paper such as P0109, as they explicitly talk about the encrypted strings use case, for example.

There are many use-cases for strong typedefs, and they have been consistently spelled out in all papers about them that we have had, including P0109 by Walter E. Brown which goes not only through the various use-cases, but also has quotes from authors of tentative libraries that tried to implement similar functionality.

All in all, it is clear to everybody that strong typedefs are simply syntactical sugar, as anything they do can be done with lots of elbow grease and lots of copy pasted code. Still I think having them would be worth it, as do many other people. Derivation does not work as it enforces an 'is-an' relationship between classes, and allows for implicit casting between classes, which proponents of strong typing explicitly don't want (unless it is explicitly allowed). Wrapping does not work since it requires forwarding lots of functions by hand. Template tags do not work since they cannot be applied to every single class in existence. The list goes on.

My personal use-case is that I maintain a library for research-level software, and I want people to have access and manipulate internals (even private) since they might need it for something. But I don't want to make everything public either. In my case a strong typedef would simply allow them to clone my original classes and mess with the internals in a controlled manner, without resorting to copy-pasting half of my library. As I said, I don't have all the use-cases in the world, but I think that in general there a relatively strong consensus that if they were somehow incorporated in the language, people would use them - also shown by the number of tentative implementations there are.

Note that I haven't proposed anything here. I was just asking questions, and I'm simply curious to know what people think the answers are, since opinions about this topic are very varied. I do have a partial implementation of my proposal, which is actually done by a simple search-and-replace script in python (so actual complexity = low), but I'm not looking to get feedback on that here.

--
You received this message because you are subscribed to a topic in the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this topic, visit https://groups.google.com/a/isocpp.org/d/topic/std-proposals/SORgghABBic/unsubscribe.
To unsubscribe from this group and all its topics, send an email to std-proposals+unsubscribe@isocpp.org.

To post to this group, send email to std-pr...@isocpp.org.

Eugenio Bargiacchi

unread,
Jun 19, 2018, 4:21:20 PM6/19/18
to ISO C++ Standard - Future Proposals
With C++17/20 aggregate initialization rules and structured bindings one can create strong types by having the wrapped type as a member. I am playing around with a single header library for exactly just that and can share the incomplete prototype (directly email me, since i might not see the mailing list reply due to my mental bandwidth). I think this might be better a library-only approach than separate syntax. And because of the different use cases, i believe different libraries, even when not standardized, can serve them better than a language solution that fits neither well enough.

I'm sending you an email now, if it works I'd be definitely happy to take advantage of it.

There have been other proposals for strong typing, ie inheriting from non-class types, i remember one from Walter Brown that did not make it through evolution working group as far as i remember. 

I know of it, and I've actually talked with Walter about it. He has been trying to get strong typedefs in C++ for quite a long time now, and he also has some pretty strong use cases to boot.
Reply all
Reply to author
Forward
0 new messages