This doc is what it is. Maybe when it is something then we'll know what it is.
Should my type be Regular (see definition in EoP)?
Regular | Irregular |
---|---|
Yes | you better have a good reason |
Everything works better with Regular types, and most of the STL assumes or works best with Regularity (although we have added some support for move-only types, etc).
See EoP.
Yes, yes it does. Don't break that. Don't forget to check that.
If there is a large cost incurred to ensure that guarantee, instead of breaking the guarantee, find a different design.
Const is tied together with ownership. A container like vector owns its elements, thus vector::begin() const returns a const_iterator which propagates constness.
A Range, however, doesn't own the elements, a range is more like a pointer. So a const Range can still range over mutable elements (like a const pointer can point to non-const data).
<insert Geoffrey here>
When should a constructor or conversion be implicit vs explicit?
Explicit | Implicit | Notes/Examples |
---|---|---|
usually | ||
if might throw | doesn't throw / noexcept | what about string from char * ? If we had string_view earlier... |
if info/accuracy is lost | if both types represent the same "platonic" thing | string/string_view/char* all represent "strings"; int, long represent "numbers" |
if performance penalty | no performance penalty (time nor space) | |
if dangerous (eg dangling pointer/ref) | not dangerous | char * from string would be performant, (mostly) accurate, noexcept; but forms a dangerous relationship |
... and should it be a separate function, or a conversion operator?
ie someByte.to_integer<int>()
or int(someByte)
?
The crux is, as above in Explicit vs Implicit, whether the two types represent the same "thing".
For example, An EmployeeRecord
and an EmployeeId
do both represent the same thing - an employee. Conversion between these makes sense.
EmployeeRecord
-> EmployeeId
could probably be implicit as there is no cost nor risk, and no info is lost (that can't be recovered).(?)
EmployeeId
-> EmployeeRecord
would be explicit as it could probably throw (probably allocates strings),
and it probably has a cost (look up Record from Id).
Now you could also probably convert a EmployeeRecord
-> PostalCode
, but that is a "hasa" relationship not an "isa".
They don't represent the same external thing.
EmployeeRecord
-> EmployeeId
may also seem "hasa" but EmployeeRecord
and EmployeeId
are actually isomorphic (modulo performance)
and do represent the same thing.
When should we use the same name (ie, typically function name), and when should we use a different name.
This applies both to same name within a class (ie overloading) and same name across classes - ie size() on containers, ie concepts/categories of classes.
Same name | different name |
---|---|
when a template would still make sense | |
same performance | |
same "intent" |
Basically, if you have a template that calls foo.size()
you have expectations on what size()
means, how it performs, etc.
Whenever we reuse a name within the standard library, we should imagine a template that uses that name. Does the template work with all uses of that name?
view see string_view
. A view
ranges over immutable elements.
span see span
(?). A span
ranges over mutable elements.
_ref ? (array_ref?)
_ptr ?
empty vs is_empty etc? get_x vs x() (and set_x(X val) vs x(X val))
<insert thoughts from "On Naming" here>
We want everything to be consistent. Sometimes this is not possible. What should we do?
The following is probably obvious when stated, but still needs to be stated sometimes:
Not all consistency is valued equally. There is a scale (from greatest to least value):
- Self consistency
- Similar consistency
- ...
- ...
- Global Consistency
For example, optional<T>
is, first and foremost, consistent with T
. (For example, see operator>=
).
Then, where possible, it is consistent with similar types (variant
, any
, expected
, smart pointers, etc)
Then, where possible, it is consistent with other std::library types.
Etc.
The standard contains many related types. It may be useful to categorize them and understand their commonalities and differences.
Aggregates: pair, tuple; struct { int m, n; }; struct { float m; }; class MyFoo {...};
Wrappers: optional, variant, any, expected, ...
Containers: int x[17], std::array, vector, vector, vector, ...
Smart Pointers: are these just wrappers? Is it a orthogonal property?