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
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:
# (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.