Strong types is a not so great name for two separate idioms:
- Named arguments: naming arguments at call site (not parameters at definition site).
Example: Rectangle(width=3, height=2) - Opaque type (sometimes strong typedef): a distinct type mimicking an existing one. This enhances type safety by limiting the domain in which a type can be used.
Example: type ItemCount = int
Both ideas are not new; they have been discussed in C++ over decades, and there have been various proposals. Boost tries emulate both, in a very ugly and compile-time-intensive manner as usual. No real solution.
I think named arguments are great. I was not missing them in C++, but after using languages that support named arguments, it's often limiting to have argument
order as the only way to carry information. It's particularly bad for repeating the same type many times:
sf::ContextSettings(0, 0, 0, 1, 1, sf::ContextSettings::Default, false); // wtf
I think opaque types are good, but not great. It makes sense to abstract to a certain level, but they are often overdone. Width/height is such an example -- both represent the same semantic type (a
distance), just along different axes. What it implies is that you will need conversions for many basic operations (like comparing width and height), yet you can still not use them to represent other distances (such as the length of a vector). It's also notable that the width/height example wouldn't be a problem in the first place in the presence of named arguments.
A more practical approach I often saw is to separate similar, but semantically different types. A good example is to split the concepts of absolute state and relative modification. This can be applied to many domains, like:
- Position + Direction (2D/3D space)
- Offset and length in an array (1D)
- Account balance and deposit/withdrawal
- ...
Where it adds type safety is in operator overloading (subtracting two positions yields a direction) and being explicit about coordinate conversions. This is
not an opaque type, because the two behave differently.
Now what can we do in C++?
For opaque types, the common workaround is just to define two separate types. Language abstraction mechanisms (templates) can help reduce code duplication. A not so good approach is
typedef/using -- it may increase code clarity, but doesn't enforce type safety.
Unfortunately, it doesn't look like opaque types are a feature that can be emulated easily -- especially once we have methods, the only resort is to use the preprocessor or templates when
defining the types. Duplicating an already existing type means copy&paste.
For named arguments, there are workarounds like builders/fluent setters:
sf::ContextSettings
.antialiasingLevel(4)
.depthBits(0)
...
or simply using the fields/setters directly:
sf::ContextSettings s;
s.antialiasingLevel = 4;
s.depthBits = 0;
...
The first approach is more flexible and allows immutable objects, but also very verbose to implement.
Regarding library support of named arguments, there are some interesting ideas, also from that blog
https://www.fluentcpp.com/2018/12/14/named-arguments-cpp. Again, nothing new, just distilled what Boost.Parameters has been doing
since 2005. The thing is, any solution that forces you to define your function as templates, is near-useless in practice, because those require definition in the header, exposition of implementation depencencies and largely increased compile times. So as nice as it looks as an academic exercise, a library like SFML couldn't switch to
this implementation of named arguments.
It would be possible to get rid of templates using type erasure, but at other costs (type-safety only dynamically, extra indirection for parameter access). Any solution that allows for switching the argument order with compile-time checks must, by principle, be one of the following:
- Overloads for all possible combinations (can be generated via preprocessor, lots of unnecessary declarations)
- Accepting generic types and resolve proper order -> requires template
A decent trade-off could be to fix the order, but require (or allow) naming parameters. This would discard all the advantages of omitting default parameters, but could still be useful. Unfortunately, even here, there is no widely standardized idiom, so you would probably confuse a lot of users with such a new syntax, and also APIs may get more complex.
One important aspect of named arguments, which is often overlooked, is however: parameter names become part of the interface. While currently only existing as a documentation help, renaming a parameter with such a feature will introduce a breaking change to function callers.
Since C++ doesn't have native support for named arguments, I would suggest that things are either done the old-fashioned way (naming functions clearly, using enums instead of bool flags, offering fluent APIs), or when being "smart" with named argument emulations, make sure they are easy to implement, for both the library and the client. For SFML, I currently don't see the big use case apart from a few places like
sf::ContextSettings, which are often instantiated using field assignments and not the constructor.