+ All Categories
Home > Documents > Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics...

Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics...

Date post: 28-Aug-2021
Category:
Upload: others
View: 2 times
Download: 0 times
Share this document with a friend
90
2 Simpler Strong Types C++now Peter Sommerlad [email protected] @PeterSommerlad https://github.com/PeterSommerlad/talks_public/tree/master/C++now/2021 Slides: 3
Transcript
Page 1: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

2

Simpler Strong Types

C++nowPeter Sommerlad

[email protected]@PeterSommerlad

https://github.com/PeterSommerlad/talks_public/tree/master/C++now/2021

Slides:

3

Page 2: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

MotivationBecause Tony van Eerd says so:

4 . 1

Tony van Eerd is a C++ friend of mine and I recommend that you watch his conference talks they are veryentertaining.

Types were invented to detect wrong and inappropriate computations in programs, i.e., incrementing a booleanvalue instead of an integer.

With types established also compiler-induced automatic type conversion became possible.

Since C++ ist statically typed, type errors are detectable at compile time and thus are easier to fix than withdynamically typed languages, where type errors often only can be detected at run-time through elaborativeautomated tests. In addition the static type checking of C++ allows for features such as overloading, selecting theappropriate operation or function for a given type, leading to its unique compile-time polymorphism and enablingthe code abstraction through templates without requiring run-time overhead for generic code.

Speaker notes

Page 3: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Motivation C++now 2021

4 . 2

Page 4: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

MotivationOrder of Argument Bug PreventionCommunicate and Check Semantics of ValuesLimit Operations to Useful subset

4 . 3

Often, strong, user-defined types for values are motivated with functions taking multiple arguments of the sametype but with different meanings. But we also look at two more important reasons for user-defined value types:communicate semantics and limit the amount of operations to a useful subset.

Speaker notes

Page 5: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Order of Argumentsdouble consumption(double litergas, double kmdriven){ return litergas/(kmdriven/100);}

void demonstrateStrongTypeProblem() { ASSERT_EQUAL(8., consumption(40,500)); ASSERT_EQUAL(8., consumption(500,40)); // which one is correct?}

4 . 4

Having a function with multiple parameters of the same or compatible types are prone to be called with the wrongorder of arguments. In our example, computing the consumption of a car from the amount of gas used vs thekilometers driven it is hard to judge, which test case is actually the intended one, unless we look at the function’simplementation. Just using the parameter name can suffice in environments where human code reviews happen forsuch things, but still any later change can still easily mess up the arguments.

Speaker notes

Page 6: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Whole Value PatternWard Cunningham - CHECKS Pattern Language http://c2.com/ppr/checks.html

Because bits, strings and numbers can be used torepresent almost anything, any one in isolation means

almost nothing.

4 . 5

Whole Value PatternWard Cunningham - CHECKS Pattern Language http://c2.com/ppr/checks.html

Because bits, strings and numbers can be used torepresent almost anything, any one in isolation means

almost nothing.

Instead of using built-in types for domain values,provide value types wrapping them.

4 . 5

Page 7: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

While the original Whole Value pattern text reflects popular object-oriented programming languages at the time.The underlying paradigm of user-defined types with corresponding operations is a major contribution of OOP and isoften taken as a given in programming languages today. C++ has the unique benefit, that such user-defined typescan be implemented without run-time overhead (modulo some bad ABI decisions for passing class objects made inthe 1990s by some vendors).

Speaker notes

Whole Value Pattern: How toDefine value types and use these as parameters.

4 . 6

Page 8: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Whole Value Pattern: How toDefine value types and use these as parameters.

Provide only useful operations and functions.

4 . 6

Whole Value Pattern: How toDefine value types and use these as parameters.

Provide only useful operations and functions.Include formatters for (input and) output.

4 . 6

Page 9: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Whole Value Pattern: How toDefine value types and use these as parameters.

Provide only useful operations and functions.Include formatters for (input and) output.Do not use string or numeric representations of thesame information.

4 . 6

We are used to use the built-in primitive types in many examples, even so as the pattern says, if a type canrepresent almost anything, it means almost nothing. Therefore, define and use your own types for your domainvalues. And in this talk, I’ll show you, with how little (own) code such is possible without run-time or spaceoverhead.

Speaker notes

Page 10: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

What to expect?some detoursdifferent options for Whole Value Pattern in C++different approaches for different C++ version

5 . 1

Alternative Strong Type FW

@foonathan: https://github.com/foonathan/type_safe@rollbear: https://github.com/rollbear/strong_type@joboccara: https://github.com/joboccara/NamedType@a_williams: https://github.com/anthonywilliams/strong_typedef

talk later today

using myint = strong::type<int, struct my_int_>;

5 . 2

Page 11: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Why another strong type FW?using myint = strong::type<int, struct my_int_>; // @rollbear

Other pre-C++17 strong typing frameworks tend to givecomplicated mangled names to the strong type, because

they rely on type aliases

5 . 3

Why another strong type FW?using myint = strong::type<int, struct my_int_>; // @rollbear

Other pre-C++17 strong typing frameworks tend to givecomplicated mangled names to the strong type, because

they rely on type aliases

Danger of unintended identical aliases

5 . 3

Page 12: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Why another strong type FW?using myint = strong::type<int, struct my_int_>; // @rollbear

Other pre-C++17 strong typing frameworks tend to givecomplicated mangled names to the strong type, because

they rely on type aliases

Danger of unintended identical aliases

C++17 brought aggregate initialization with inheritance and structured bindings

5 . 3

One can use the inheritance trick instead of the type alias to get better names with the other frameworks as well,however, that means one needs to specify constructors, i.e., by inheriting them, like in the following exampleprovided by @rollbear:

Speaker notes

struct literPer100km : strong::type<double, literPer100km>{ using strong::type<double, literPer100km>::type; // inherit ctor(s) };

Page 13: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Things I wished to try (C++17)structured bindings

opportunity: generic mix-insauto [value] = param;

extended aggregatesopportunity: no constructorno implicit conversion, but from {} initializer

5 . 4

Things I wished to try (C++17)structured bindings

opportunity: generic mix-insauto [value] = param;

extended aggregatesopportunity: no constructorno implicit conversion, but from {} initializer

C++20 (separate branch):

operator<=>constraints

5 . 4

Page 14: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Only Useful Operations ?Provide what is really needed

6 . 1

Only Useful Operations ?Provide what is really needed

Prevent accidental expression errors

6 . 1

Page 15: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Only Useful Operations (1)ASSERT_EQUAL(42.0, 7. * ((((true << 3) * 3) % 7) << true));

6 . 2

Only Useful Operations (1)ASSERT_EQUAL(42.0, 7. * ((((true << 3) * 3) % 7) << true));

C++ has too many operations for its built-in types

unary: + - ++ -- ~ ! * &binary: + - * / % && ||& | ^ << >> == != < <= > >= <=> []

and allows to mix different types with them throughintegral promotion and implicit arithmetic

conversions.

6 . 2

Page 16: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Unfortunately, C++ suffers from its C legacy with a plethora of operations available for built-in types and worse, withimplicit conversions that too easily allows one to mix seemingly inappropriate types in a single expression. Forexample, if a variable is used to represent a hardware register with flags, the bit-operations might be useful for thisvariable’s type, but it is a strange thing to multiply such a variable with PI(π), a floating point number. So restrictingthe possible operations while still allow for intuitive use of operators is a useful property of user-defined value typesfurther limiting the chance for errors, that might otherwise go undetected at compile time.

Speaker notes

Only Useful Operations (2)

linear operations are common: + - with scalar: * /

auto four = !! + !!; // ✅ additionauto what = !! + ###; // ❌auto huh = !! * !!!; // ❌auto six = 2 * !!!; // ✅ skalar multiplication

6 . 3

Page 17: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

It makes sense to add two apples and two apples to obtain four apples. However, adding apples and pears mightbe ok for a fruit juice but not in general, because they describe semantically different things. So it would be nice ifthe plus operator would not work in that case. Also multiplying two apples by three apples is hardly useful. Whilemultiplying 2 meters by 3 meters might be useful, the resulting value must also take into account a change inmeaning to be 6 square meters. Hoever, doubling or halfing the amount of apples is a useful operation in addition toaddition and subtraction. Such skalar multiplication and addition/substraction abilities, while staying the domain ofthe type is very common, therefore, it may be nice to allow such grouping of operations, if a more limited set is notuseful.

Also, because we want to test our code and because the strong types we are talking about here, should becomparable, we see now.

Speaker notes

P0109 Opaque Typedefs

Function Aliases + Extended Inheritance = Opaque Typedefs (aka Strong Types)

7 . 1

Page 18: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

P0109 Goalscommon goals

distinct arguments for template instantiation

using OT = // opaque type;using ULT = // underlying type;not is_same_v<OT,ULT>typeid(OT) != typeid(ULT)sizeof(OT) == sizeof(ULT)

7 . 2

P0109 Goals not in PSSSTnot (directly) achievable/desired by pssst:

traits std::is_xxx_v have same valueimplicit convertibility for overload resolution

but opaque better matchinter-“cast-ability” (keeping constness)pointer-interconvertibility (reference-related)

7 . 3

Page 19: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

P0109 Simple Example

visibility defines convertibilityoperator definitions (can be = default)

using opaq = public int { opaq operator+(opaq o) { return opaq{ +int{o} }; }};opaq o1 = 16;auto o2 = +o1; // type of o2 is opaq

7 . 4

P0109 Energy Example

energy is a type with linear operations

using energy = protected double { energy operator+ (energy , energy) = default; energy& operator*=(energy&, double) = default; energy operator* (energy , energy) = delete; energy operator* (energy , double) = default; energy operator* (double , energy) = default;};

energy e{1.23}; // okay; explicitdouble d{e}; // okay; explicitd = e; // error; protected disallows implicit type adjustment heree = e + e; //okay; sum has type energye = e * e; // error; call to deleted function e *= 2.71828; // okay

7 . 5

Page 20: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

P0109 Energy Example (2)Opaque types can be recursive:

using thermal = public energy;using kinetic = public energy;thermal t{· · ·}; kinetic k{· · ·};e = t; e = k; // both okay; public allows type adjustmentt = e; t = k; // both in error; the adjustment is only unidirectionalt=t+t; k=k+k; //okay; public implies default trampolinese = t + k; // okay; calls the underlying trampoline

7 . 6

My comments on P0109

7 . 7

Page 21: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

My comments on P0109When it was presented, I liked it a lot

7 . 7

My comments on P0109When it was presented, I liked it a lot

earlier N-numbered papers exist (not actual R0)!

7 . 7

Page 22: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

My comments on P0109When it was presented, I liked it a lot

earlier N-numbered papers exist (not actual R0)!requires explicit trampoline definitions

7 . 7

My comments on P0109When it was presented, I liked it a lot

earlier N-numbered papers exist (not actual R0)!requires explicit trampoline definitions

unless already “inherited”

7 . 7

Page 23: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

My comments on P0109When it was presented, I liked it a lot

earlier N-numbered papers exist (not actual R0)!requires explicit trampoline definitions

unless already “inherited”or one gets all operators (?)

7 . 7

My comments on P0109When it was presented, I liked it a lot

earlier N-numbered papers exist (not actual R0)!requires explicit trampoline definitions

unless already “inherited”or one gets all operators (?)

not enough appreciation by EWG

7 . 7

Page 24: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

My comments on P0109When it was presented, I liked it a lot

earlier N-numbered papers exist (not actual R0)!requires explicit trampoline definitions

unless already “inherited”or one gets all operators (?)

not enough appreciation by EWGmore pressing matters at the time

7 . 7

My comments on P0109When it was presented, I liked it a lot

earlier N-numbered papers exist (not actual R0)!requires explicit trampoline definitions

unless already “inherited”or one gets all operators (?)

not enough appreciation by EWGmore pressing matters at the timeimpact hard to judge

7 . 7

Page 25: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

My comments on P0109When it was presented, I liked it a lot

earlier N-numbered papers exist (not actual R0)!requires explicit trampoline definitions

unless already “inherited”or one gets all operators (?)

not enough appreciation by EWGmore pressing matters at the timeimpact hard to judge

function aliases as a tool for concept maps (?)

7 . 7

My comments on P0109When it was presented, I liked it a lot

earlier N-numbered papers exist (not actual R0)!requires explicit trampoline definitions

unless already “inherited”or one gets all operators (?)

not enough appreciation by EWGmore pressing matters at the timeimpact hard to judge

function aliases as a tool for concept maps (?)partially solvable with named lambda objects

7 . 7

Page 26: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

My comments on P0109When it was presented, I liked it a lot

earlier N-numbered papers exist (not actual R0)!requires explicit trampoline definitions

unless already “inherited”or one gets all operators (?)

not enough appreciation by EWGmore pressing matters at the timeimpact hard to judge

function aliases as a tool for concept maps (?)partially solvable with named lambda objectsnot covered here

7 . 7

Simplest Strong Typing

8 . 1

Page 27: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Simplest Strong Typing (1)

struct8 . 2

Just using a struct to wrap a single member of a built-in type, such as double makes a distinct type for our domainvalue we want to represent.

You might ask, why not just use the long-term practice of type aliases/typedef names. Those do not introducenew types and go no further than parameter names to distinguish your domain types.

A big advantage of this simples strong type approach is that it is also feasible in C to prevent wrongly typed/orderedfunction arguments.

Speaker notes

Page 28: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Simplest Strong Typing (2)Just use a struct - works even in C!

struct literGas{ double value;};struct kmDriven{ double value;};double consumption(literGas liter, kmDriven km){ return liter.value/(km.value/100);}

8 . 3

By just wrapping the values for the domain types in a struct allows us to eliminate the possible confusion (almost).At least it also allows us to overload both sequences thus making the code implicitly correct. However, theimplementation got a bit more complex, because we now have to obtain the double value from the wrapper type.We will see later how this can be circumvented by mixing-in operators as hidden friends.

Speaker notes

Page 29: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Simplest Strong Typing (3)Solution is not perfect, yet.

void demonstrateStrongTypeProblem() { literGas consumed{40}; kmDriven distance{500}; ASSERT_EQUAL(8., consumption(consumed,distance)); ASSERT_EQUAL(8., consumption({500},{40})); // still not perfect, but needs at least {}}

8 . 4

However, if we do not provide an overload, then the implicit conversion from an initializer list with a single element,still allows to write hard to judge function calls. But at least, these calls are not taking arbitrary numbers anymore. Away to circumvent this ability is to provide the second overload (and make it deleted), to avoid having calls thewrong way around or using the implicit conversion. Those would fail due to ambiguity.

Speaker notes

Page 30: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Simplest Strong Typing (4)consumption() should return also a strong type!

literper100km consumption(literGas liter, kmDriven km){ return {liter.value/(km.value/100)}; // braces needed (aggregate initialization)}void demonstrateStrongTypeProblem() { literGas consumed{40}; kmDriven distance{500}; ASSERT_EQUAL(literper100km{8}, consumption(consumed,distance)); // error: no match for 'operator=='}

8 . 5

Unfortunately, for we do not get the equality comparison for our simplest strong types. However, C++20 provides away to get comparison operators generated by the compiler with =default. In C++17 we need to provide our owncomparison.

Speaker notes

Page 31: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Simplest Strong Typing (5)Too simple DIY comparison (see later)

struct literper100km{ double value; constexpr bool operator==(literper100km const & r) const & noexcept { // make it symmetric return value == r.value; // shady bc double } constexpr bool operator!=(literper100km const & r) const & noexcept { return ! (*this == r); }};

8 . 6

Too simple comparison for C++17, makes test case compile.

Speaker notes

Page 32: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Formatter for Output

duplication needed per strong typecould use unit as decoration

std::ostream& operator<<(std::ostream &out, literGas const &val) { return out << val.value;}std::ostream& operator<<(std::ostream &out, kmDriven const &val) { return out << val.value;}std::ostream& operator<<(std::ostream &out, literper100km const &val) { return out << val.value;}

8 . 7

For testing and other purposes it would be nice to be able to output the value of our strong type. This allows tounderstand a test failure for example, because we can inspect the mismatching value directly from the test caseoutput.

But providing an overload for each strong type class in the same namespace the the class is tedious. A benefit ofthis approach is that we can provide a unit indicator in the output, i.e. ‘l’ for literGas ‘km’ for kmDriven, and ‘l/100km’for our consumption computation result.

Speaker notes

Page 33: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Eliminate DuplicationDRY simplest strong types to make them simpler

9 . 1

Too much boilerplate code is tedious and often error prone due to code cloning and modification.

Let us look at approaches to eliminate such boilerplate code.

Speaker notes

Page 34: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Option: Function Templates

must be in namespace of value classes for ADLmay be selected by too many classes(concepts can help - is_strong_v<T>)assumes specific public member variable(works before C++17)

// full generictemplate<typename T>std::ostream& operator<<(std::ostream &out, T const &val) { return out << val.value;}

9 . 2

C++ templates are the key to avoid copy-paste for code that is only different with respect to the types used. Wealso benefit from C++’s template argument deduction which allows us to just call a function template without theneed to specify its template arguments.

Speaker notes

Page 35: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

CRTP - Curiously Recurring TemplateParameter Pattern

template<typename derived>//struct base {// provide something with and for derived};struct X : base<X>{};struct Y : base<Y>{};

Often for mix-ins for derived into multiple derived(as Hidden Friends)

9 . 3

CRTP - Curiously Recurring TemplateParameter Pattern

template<typename derived>//struct base {// provide something with and for derived};struct X : base<X>{};struct Y : base<Y>{};

Often for mix-ins for derived into multiple derived(as Hidden Friends)

Can’t constrain derived because it is incomplete

9 . 3

Page 36: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

CRTP - Curiously Recurring TemplateParameter Pattern

template<typename derived>//struct base {// provide something with and for derived};struct X : base<X>{};struct Y : base<Y>{};

Often for mix-ins for derived into multiple derived(as Hidden Friends)

Can’t constrain derived because it is incomplete

C++17 allows derived classes to remain an aggregate9 . 3

The “Hidden Friends” pattern for operator overloads is also known as the “Barton-Nackman trick”. It can be applieddirectly within a class, such as putting our friend operator<< from above within the class itself.

But for generic inclusion of such hidden-friend operators we apply the CRTP pattern and define the friend operatoroverloads in a future base class. Since this class does not define any data members it is an “empty base class”.Such empty base classes do not occupy extra space in the object of a derived class.

From C++17 on such empty base classes allow the derived class still to be considered an aggregate. The latter hasefficiency benefits for the generated code (trivial copying, potential to pass in registers).

Speaker notes

Page 37: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

CRTP for output (1)template<typename U>struct Out { friend std::ostream& operator<<(std::ostream &out, U const &r) { // operator overload as hidden friend return out << r.value; // assumes member .value }};struct literper100km: Out<literper100km> { // CRTP Patterndouble value; };

9 . 4

Here we see the CRTP pattern applied to provide an output operator for our simple strong type, by inheriting from it.

In this implemenation the CRTP class template assumes all derived classes will provide a data member that hasthe name value and who’s type has a corresponding output operator, such as all the built-in types have.

This generic programming by having a member name convention can be used since C++98 and is the key tomaking templates work. However, it also limits the flexibility of the programmer and can result in interestingtemplate instantiation error messages, when a typo results in a name mismatch.

C++20 concepts in principle allow a way out, by requiring such properties from their template parameters. However,in the case of the CRTP pattern the template parameter cannot be checked when the class template is instantiated,because in that situation the template argument (literper100km) is still incomplete and thus a requires clausecannot check its properties.

Speaker notes

Page 38: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

CRTP for output (2)Apply structured binding for arbitrary member name.

template<typename U>struct Out { friend std::ostream& operator<<(std::ostream &out, U const &r) { auto const& [v] = r; // structured binding return out << v; }};

Derived class (U) must have a single public membervariable with defined output operator.

9 . 5

If we do not want to commit on a naming scheme for our strong type data member, we can assume that it has asingle public member variable. This allows us to use C++17 structured binding to access this sole data member in ageneric way for all our operator mix-ins without the need for a common naming convention.

Speaker notes

Page 39: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Intermezzo: Structured BindingDecompose aggregates on the fly:struct, std::tuple<>, std::pair<>

# of names in [] = # data members/array elementsusually auto (value) or auto const &auto & only if function returns lvalue-referencenot (yet) possible: auto [x...] = f()

auto [x,y] = f_returningstruct();

9 . 6

Structured bindings are usable with tuples and pair, but the most benefit we get from applying it to classes withpublic data members, because a named type and data member can communicate its purpose better than abstractnames like first and second or std::get<0>(tuple).

Unfortunately, we must know the “arity” of the type (number of non-static data members) and all data membersmust be public to make that work.

Speaker notes

Page 40: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

CRTP for output (3)

Employ a naming convention for prefix and suffix

template <typename U>struct Out{ friend std::ostream& operator<<(std::ostream &out, U const &r) { if constexpr (detail_::has_prefix<U>{}){ out << U::prefix; } auto const &[v]=r; out << v; if constexpr (detail_::has_suffix<U>{}){ out << U::suffix; } return out; }};

9 . 7

Using a name convention we can drag in a prefix and/or suffix static member from the strong type to allow fordecorating our strong type’s output. For example, literGas will use a suffix string of “l” on output.

the conditions has_prefix and has_suffix use the Detection Idiom to determine the availability of thecorresponding static class member.

Speaker notes

Page 41: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Using CRTP for outputstruct literper100km : Out<literper100km> { double value; constexpr static inline auto prefix="consumption "; constexpr static inline auto suffix=" l/100km";};

void demo_output_crtp(){ literper100km consumed{{},9.5}; // ugly... see later std::ostringstream out{}; out << consumed; ASSERT_EQUAL("consumption 9.5 l/100km", out.str());}

9 . 8

A trait to check for prefix/suffix

similar for suffix, actually checks

decltype(std::declval<std::ostream&>() << U::prefix)

Detection Idiom demonstration

template<typename U, typename = void>struct has_prefix : std::false_type {};

template<typename U>struct has_prefix<U, std::void_t<decltype(U::prefix)>> // <-- check: std::true_type {};

9 . 9

Page 42: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

the more specialized template wins in the if constepxr instantiation

Speaker notes

Detection Idiom vs C++20 concept

checks for possibility of expression, similar to concept

template<typename U, typename = void>struct has_prefix: std::false_type {};template<typename U>struct has_prefix<U, std::void_t<decltype(U::prefix)>> : std::true_type {};

template<typename U>concept has_suffix = requires (U u) { U::suffix; };

9 . 10

Page 43: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

More Operators via CRTP

10 . 1

Remember DIY Comparisonstruct literper100km{ double value; constexpr bool operator==(literper100km const & r) const & noexcept { // make it symmetric return value == r.value; // shady bc double } constexpr bool operator!=(literper100km const & r) const & noexcept { return ! (*this == r); }};

10 . 2

Page 44: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Too simple comparison for C++17, makes test case compile.

Requires code duplication in all types -> CRTP mix-ins

Speaker notes

Mix-in Comparison C++17template <typename U>struct Eq{ friend constexpr bool operator==(U const &l, U const& r) noexcept { auto const &[vl]=l; auto const &[vr]=r; return vl == vr; } friend constexpr bool operator!=(U const &l, U const& r) noexcept { return !(l==r); }};

Equality should never compare different strong types

10 . 3

Page 45: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

comparing different numeric types for equality can have merit, e.g., against zero, but values from different domain-specific types should never compare.

Speaker notes

Mix-in Relations C++17template <typename U>struct Order: Eq<U> { friend constexpr bool operator<(U const &l, U const& r) noexcept { auto const &[vl]=l; auto const &[vr]=r; return (vl < vr); } friend constexpr bool operator>(U const &l, U const& r) noexcept { return r < l; } friend constexpr bool operator<=(U const &l, U const& r) noexcept { return !(r < l);

unfortunately C++20 <=> cannot be defaulted

10 . 4

Page 46: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

operator<=>(T const&) const =default; only works within class T. Since the CRTP argument will beincomplete, when the class is compiled, we cannot default the comparison operators.

Speaker notes

CRTP for simple arithmetic

Arithmetic operators should also allow thecorresponding assignment.

template <typename R>struct Add { friend constexpr R& operator+=(R& l, R const &r) noexcept { auto &[vl] = l; auto const &[vr] = r; vl += vr; return l; } friend constexpr R operator+(R l, R const &r) noexcept { return l+=r; }}; // similar: Subtraction

10 . 5

Page 47: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

As with boost operators.hpp library binary arithmetic operators can be defined based on their combined assignmentoperator. But with CRTP we do not even need a subclass define the combined assignment, but can genericallyimplement it, when we can live without information hiding. Remember, we are implementing types that replace built-in type usage and thus encapsulation was never given for them.

Incrementing, decrementing similarly.

Also the noexcept can be/is a conditional noexcept based on the underlying operation but that doesn’t fit this slide

Speaker notes

CRTP gotchatemplate <typename R>struct Add {...};

We cannot constrain the mix-in class parameter

10 . 6

Page 48: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

CRTP gotchatemplate <typename R>struct Add {...};

We cannot constrain the mix-in class parameter

When instantiated, argument type is still incomplete struct energy : Add<energy>

10 . 6

CRTP for scalar multiplication (1)template <typename R, typename SCALAR>struct ScalarMultImpl { friend constexpr R& operator*=(R& l, SCALAR const &r) noexcept { auto &[vl]=l; vl *= r; return l; } friend constexpr R operator*(R l, SCALAR const &r) noexcept { return l *= r; } friend constexpr R operator*(SCALAR const & l, R r) noexcept { return r *= l; }};

cannot derive SCALAR automatically from R

10 . 7

Page 49: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

For scalar multiplication we need a way to specify the underlying member type and then provide multiplication withit.

Speaker notes

CRTP for scalar multiplication (2)

We get rid of the many {} soon.

struct kmDriven: Out<kmDriven>, ScalarMultImpl<kmDriven,double> { double km;};literper100km consumption(literGas l, kmDriven km) { return {{},{},l.liter/(km*0.01).km}; // multiply km by 0.01}void demonstrateStrongTypeProblem() { literGas l{{}, 40}; kmDriven km{{}, {}, 500}; ASSERT_EQUAL(literper100km({}, {}, 8.), consumption(l, km));}

10 . 8

Page 50: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

However, our mix-in classes would need to provide the factor type but still allow mixing in the derived class. Toprovide this in a generic way, we use a meta-programming template argument binder and a template alias.

Speaker notes

Bit operatorsonly useful if data member is unsigned

similarly for & and ^

template <typename R>struct BitOps { friend constexpr R& operator|=(R& l, R const &r) noexcept { auto &[vl]=l; static_assert(std::is_unsigned_v<underlying_value_type<R>>, "bitops are only be enabled for unsigned types"); auto const &[vr] = r; vl |= vr; return l; } friend constexpr R operator|(R l, R const &r) noexcept { return l|=r; }

10 . 9

Page 51: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

The static_assert within the body of the operator function is possible, because whenever that is instantiated,the template argument type is complete.

Speaker notes

Shift operatorstemplate<typename L, typename B=unsigned int>struct ShiftOps{friend constexpr L& operator<<=(L& l, B r) { auto &[vl]=l; using TO = underlying_value_type<L>;

static_assert(std::is_unsigned_v<TO>); if constexpr (std::is_unsigned_v<B>){ pssst_assert(r <= std::numeric_limits<TO>::digits); vl = static_cast<TO>(vl << r); } else { auto const &[vr] = r; pssst_assert(vr <= std::numeric_limits<TO>::digits); vl = static_cast<TO>(vl << vr); } return l;

10 . 10

Page 52: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Making Mix-ins work forSimple Strong Types

11 . 1

Problems to overcome1. Multiple operations require multiple bases

struct lp100km: Cmp<lp100km>, Out<lp100km>repeating the same CRTP argumentmore {} needed for init

11 . 2

Page 53: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Problems to overcome1. Multiple operations require multiple bases

struct lp100km: Cmp<lp100km>, Out<lp100km>repeating the same CRTP argumentmore {} needed for init

2. Empty bases need leading {} for initliterper100km({}, {}, 8.1)trailing empty braces can be elided

11 . 2

Problems to overcome1. Multiple operations require multiple bases

struct lp100km: Cmp<lp100km>, Out<lp100km>repeating the same CRTP argumentmore {} needed for init

2. Empty bases need leading {} for initliterper100km({}, {}, 8.1)trailing empty braces can be elided

3. What are useful operator combinations?Bit operations only for unsigned typesLinear operations for numbersHow to avoid repeating the wrapped type for scalar ops

11 . 2

Page 54: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Combining mix-in bases

takes a strong type “derived class” Utakes a list of mix-in base class templatesinstantiates all bases with U

// apply multiple operator mix-ins and keep this an aggregatetemplate <typename U, template <typename ...> class ...CRTP>struct ops : CRTP<U>...{};template <typename U>using Additive=ops<U,UPlus,UMinus,Abs,Add,Sub,Inc,Dec>;

struct liter : ops<liter,Additive,Order,Out>{ double l{};};

11 . 3

see how we can use template aliases to abbreviate useful combinations the same class template ops also allows toadd more to a specific subclass

Speaker notes

Page 55: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Simple Init of SST (try 1)1. define an explicit constructor

requires code in SST classmember public for structured binding (C++17)

struct literGas : ops<literGas,Additive,Order,Out> { constexpr explicit literGas(double lit):l{lit}{}; double l{};};

prevents: return {wrapped_value};

11 . 4

Using an explicit constructor prevents implicit conversions. However, it also means that the type is no longer anaggregate. You might even want to provide a default argument to the constructor parameter to keep the defaultconstructor available. Or you need to resurrect the default constructor by

Speaker notes

constepxr literGas() = default;

Page 56: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Simple Init of SST (try 2)2. put data member in first base class object

all following sub-objects are emptyempty trailing subobjects do not require {}

template <typename V, typename TAG>struct holder { static_assert(std::is_object_v<V>, "no references or incomplete

types allowed"); using value_type = V; V value { }; // public for aggregate};struct literGas : holder<double,literGas>, ops<literGas,Additive,Order,Out> {};

11 . 5

Having the sole data member in the first base class means als other empty bases do not require a pair of braces forinitialization. The default init of the value will prevent using uninitializes data.

This version still requires duplication…

Speaker notes

Page 57: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Simple Init of SST (best)3. put data member in first base class object

and combine that with opsother short-hands can use that

template <typename V, typename TAG, template<typename...>class ...OPS>struct strong : detail_::holder<V,TAG>,ops<TAG,OPS...> {};struct literGas : strong<double,literGas,Additive,Order,Out>{};

that is as simple as it gets, modulo predefinedcombinations.

11 . 6

Having the sole data member in the first base class means als other empty bases do not require a pair of braces forinitialization. The default init of the value will prevent using uninitializes data.

Speaker notes

Page 58: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Different Inits for returntemplate <typename T, typename S = underlying_value_type<T>>constexpr auto retval(S && x) noexcept(std::is_nothrow_constructible_v<T,S> ) { if constexpr (needsbaseinit<T>{}) return T{{},std::move(x)}; // value in most derived else return T{std::move(x)}; // value in base or ctor}template <typename U>struct UMinus{ friend constexpr U operator-(U const &r) noexcept(noexcept(-

std::declval<underlying_value_type<U>>())){ auto const &[v]=r; return retval<U>(-v);

Useful to accomodate aggregates without constructor

11 . 7

Caution: this might be over-engineered

A strong type might need to be initialized with a leading brace for its ops<> empty base, or without, when its using strong<> as its first base class or when it has a constructor. This is needed to compute the return value from themix-in operator functions. To ease that distinction, we use the retval function template. This is mainly needed inunary operators and for wrapping functions.

Speaker notes

Page 59: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Trait for determining init possibility

Over-engineered, but allows for migration fromsimplest strong types.

template <typename T, typename= void>struct needsbaseinit:std::false_type{};

template <typename T>struct needsbaseinit<T,std::void_t<decltype(T{{},std::declval<underlying_value_type<T>>()})>>:std::is_aggregate<T>{};

false: construtor or first base holds value

true: single base is empty, aggregate class holds value

11 . 8

Mapping mathematical functionsAbs is a model for further math functions

Not all should return the strong type, but those forrounding are (ceil, floor, trunc, round,

nearbyint, rint)

template <typename U>struct Abs{ // part of Additive for unit tests comparing float types friend constexpr U abs(U const &r) { auto const &[v]=r; using std::abs; // allows stacking of strong types return retval<U>(abs(v)); }};

11 . 9

Page 60: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Other math functions like exponential and logarithm functions, or trigonometric functions fundamentally change thedomain, therefore should return plain type, not the strong type.

Speaker notes

Mapping functions macro#define MakeUnaryMathFunc(fun) \ friend constexpr U \ fun(U const &r) {\ auto const &[v]=r;\ using std::fun;\ return detail_::retval<U>(fun(v)); \}

for linear functions returning the strong type seems OK

template <typename U>struct Abs{ // is part of Additive for Unit Tests MakeUnaryMathFunc(abs)};

11 . 10

Page 61: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Mapping functions macro (plain)#define MakeUnaryMathFuncPlain(fun) \ friend constexpr auto \ fun(U const &r) {\ auto const &[v]=r;\ using std::fun;\ return fun(v); \}

when returning the strong type is inappropriate

template <typename U>struct RootPlain{ MakeUnaryMathFuncPlain(sqrt) MakeUnaryMathFuncPlain(cbrt)};

11 . 11

Scalar Multiplication (1)scalar modulo operation only for integral scalars

template <typename R, typename SCALAR, bool=false>struct ScalarModulo{}; // no operator% for non-integral types

template <typename R, typename SCALAR>struct ScalarModulo<R,SCALAR,true>{ friend constexpr R& operator%=(R& l, SCALAR const &r) { static_assert(std::is_integral_v<SCALAR>); auto &[vl]=l; static_assert(std::is_integral_v<decltype(vl)>); pssst_assert(r != decltype(r){}); // division by zero vl %= r; return l; }

11 . 12

Page 62: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

scalar multiplication for integers should also allow modulo operation for (unsigned?) integers.

what hits again, is that one cannot constrain a CRTP parameter directly.

Speaker notes

Scalar Multiplication (2)template <typename R, typename SCALAR>struct ScalarMultImpl : ScalarModulo<R,SCALAR, std::is_integral_v<SCALAR> && not std::is_same_v<std::decay_t<SCALAR>,bool>> { using scalar_type=SCALAR; friend constexpr R& operator*=(R& l, SCALAR const &r) noexcept { auto &[vl]=l; vl *= r; return l; } friend constexpr R operator*(R l, SCALAR const &r) noexcept { return l *= r; }

prevent scalar modulo to be used on bool and floatingpoint numbers

11 . 13

Page 63: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

division operation needs to check for divide by zero

std::decay_t<SCALAR> is a bit of overkill, but I wanted to prevent using std::remove_cv_ref_t

Speaker notes

Scalar Multiplication (3)How to prevent repeating scalar type

template<typename B, template<typename...>class T>struct bind2{ template<typename A> using apply=T<A,B>;};template<typename SCALAR>using ScalarMult = bind2<SCALAR,ScalarMultImpl>;

template <typename TAG, typename SCALAR, template<typename...>class ...OPS>

using LinearOps = ops<TAG, ScalarMult<SCALAR>::template apply, Additive, Order, Value, OPS... >;template <typename BASE typename TAG, template<typename...>class

...OPS>using Linear=strong<BASE, TAG, ScalarMult<BASE>::template apply,

11 . 14

Page 64: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

the ops<> and strong<> templates expect unary traits that take the strong type as their sole argument.

we use template meta-programming to bind a second type argument to return a single-parameter template forscalar multiplication.

can not use underlying_value_type, because V is still incomplete when the base classes are defined

Speaker notes

// the following more generic impl only works on gcc... and is illegal// http://eel.is/c++draft/temp.res.general#6.3// template<typename A, typename ...C>// using apply=T<A,B,C...>;// this simpler and sufficient version also supported by msvc and clang

Scalar Multiplication (3-1)we lack strong template aliases…

ScalarMult<SCALAR>::template apply

bind2 must be a class with inner template alias apply

requires usage of ::template

11 . 15

Page 65: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Strong types, simple?struct literGas { double value;};struct literGas : strong<double, literGas, Additive, Order, Out> { constexpr static inline auto suffix = " l";};struct literGas : ops<literGas, Additive, Order, Out> { constexpr explicit literGas(double lit={}) : l{lit} {} double l; constexpr static inline auto suffix = " l";};

11 . 16

SST Mix-in Design Optionsfor mix-in hidden friends with CRTP bases

Version possible design

C++11 member name convention(can be private)

C++17 public member withstructured binding & initialbase class with member

C++20 plus operator<=>

11 . 17

Page 66: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Linear spaces

displacementvector spaceno originptrdiff_tdifference_type

positionaffine spacedefinitive originsize_tsize_type

Often the same type is used for a vector space as well asaffine space

12 . 1

vector space -> affine space

vector spaceduration

affine spacetime_point

time_point - time_point –> durationtime_point + duration –> time_point

time_point + time_point –> %&

#include <chrono>

12 . 2

Page 67: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Underappreciated linear spacesstandard library suffers from it (from vector)

size_type __n = std::distance(__first, __last); // implicit conversion if (capacity() - size() >= __n) // aha to avoid warning in comparison { std::copy_backward(__position, end(), this->_M_impl._M_finish + difference_type(__n)); // cast to the real thing again

12 . 3

providing associated 1D affine space// vector space degrees for (K and °C)struct degrees: Linear<double, degrees, Out>{};struct Kelvin : affine_space_for<Kelvin,degrees> { static constexpr auto suffix="K";};struct CelsiusZero { // cannot be just a value (C++17) constexpr degrees operator()() const noexcept{ return degrees{273.15}; }};struct Celsius:affine_space_for<Celsius,degrees,CelsiusZero> { static constexpr auto suffix="°C";};

12 . 4

Page 68: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Unfortunately C++17 does not allow floating point types to be used as template parameters. Therefore, we need topass the origin CelsiusZeroas a function object.

Speaker notes

Using vector and affine spacesvoid thisIsADegreesTest() { degrees hotter{20}; // vector space Celsius spring{15}; // affine space ASSERT_EQUAL(Celsius{35},spring+hotter);}void testCelsiusFromKelvin(){ Kelvin zero{273.15}; zero += degrees{20}; ASSERT_EQUAL(Celsius{20},convertTo<Celsius>(zero));}

12 . 5

Page 69: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

1D affine space - howtemplate <typename POINT, typename VECTOR_SPACE, typename ZEROFUNC=default_zero<VECTOR_SPACE>>struct affine_space_for : ops<POINT,Order, Value, Out>{ static inline constexpr auto using vector_space=VECTOR_SPACE; using point=POINT; using value_type=underlying_value_type<vector_space>; origin() noexcept{ return point{detail_::retval<vector_space>(ZEROFUNC{}())};} constexpr affine_space_for(vector_space v=origin()) noexcept:value{v}{} constexpr affine_space_for(value_type v) noexcept:value{detail_::retval<vector_space>(v)}{} vector_space value; // point + vector friend constexpr point& operator+=(point& l, vector_space const &r) // ... friend constexpr point operator+(point l, vector_space const &r) // ... friend constexpr point operator+(vector_space const & l, point r) // ... // point - vector // caution check if before origin is allowed overflow check friend constexpr point& operator-=(point& l, vector_space const &r) // ... friend constexpr point operator-(point l, vector_space const &r) // ... // point - point = vector friend constexpr vector_space operator-(point const &l, point const &r) // ...};

12 . 6

1D affince spaces - conversionsconstexpr TO convertTo(FROM from) noexcept{ static_assert(std::is_same_v< typename FROM::vector_space ,typename TO::vector_space>); return detail_::retval<TO>((from.value-(value(TO::origin())-

value(FROM::origin()))));}

affine spaces must be from same vector space

NOTODO: conversion with factor: use a units frameworkinstead!

12 . 7

Page 70: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Obtaining the frameworkPeter Sommerlad’s Simple Strong Types frameworkPSsst available on github.Single header library with a variety of features andfreely usableVersions for C++17 (main branch) and C++20(branch).A version for C++14 uses .value as the datamember name convention.

12 . 8

SummaryP0109 was a good attempt, but failedA library solution allows simpler strong typingNaming the domain type allows for nicer mangling

ps::consumption(ps::literGas, ps::kmDriven)

vs (for example @rollbear’s )

bf::consumption(strong::type<double, bf::liter_tag>, strong::type<double, bf::km_tag>)

https://godbolt.org/z/T1qbMbh3a

12 . 9

Page 71: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

https://godbolt.org/z/T1qbMbh3a

Speaker notes

Questions & ContactPeter Sommerlad

[email protected]@PeterSommerlad

https://sommerlad.ch

PSsst:

https://github.com/PeterSommerlad/PSsst

13

Page 72: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

ExtrasA better Bool than bool-> TL;DR: it’s not

C++20 3-way comparison mix-in-> TL;DR: some surprises

why aggregates?

14

replacing boolA failed idea…

15 . 1

Page 73: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Remember DIY Comparisonstruct literper100km{ double value; constexpr bool operator==(literper100km const & r) const & noexcept { // make it symmetric return value == r.value; // shady bc double } constexpr bool operator!=(literper100km const & r) const & noexcept { return ! (*this == r); }};

15 . 2

Too simple comparison for C++17, makes test case compile.

Requires code duplication in all types -> CRTP mix-ins

Speaker notes

Page 74: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Mix-in Comparison C++17template <typename U>struct Eq{ friend constexpr bool operator==(U const &l, U const& r) noexcept { auto const &[vl]=l; auto const &[vr]=r; return vl == vr; } friend constexpr bool operator!=(U const &l, U const& r) noexcept { return !(l==r); }};

15 . 3

A potential problem

Can we do better?

static_assert(std::is_arithmetic_v<bool>);

constexpr literper100km l12{{},12}, l24{{},24};

static_assert(2 * (l12 != l24));

bool promotes to int

arithmetic on bool possible

15 . 4

Page 75: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Bool: a better bool?

no integral promotionusable only in boolean contextsno implicit conversionshortcut evaluation possible?

Jonathan Müller (@foonathan) has a similar special boolean type in his type_safe library that I was not aware

of

Goals

15 . 5

Bool how?struct Bool { constexpr Bool() noexcept=default; constexpr Bool(bool const b) noexcept : val { b } { } constexpr explicit operator bool() const noexcept { return val; } bool val{};};

usable in conditions and && || ?:

15 . 6

Page 76: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

The explicit conversion operator allows using it in all conditions of statements and with logical operators.

Speaker notes

Bool how? (operators)struct Bool { friend constexpr Bool operator==(Bool const &l, Bool const& r) noexcept { return Bool{l.val == r.val}; } friend constexpr Bool operator!=(Bool const &l, Bool const& r) noexcept { return !(l==r); } friend constexpr Bool operator !(Bool const &l){ return Bool{! l.val}; }

no binary logical operators, would loose shortcut eval

15 . 7

Page 77: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

operator!= corresponds to a logical XOR operation.

the operator not is not critical and lets the type remain Bool.

Speaker notes

Bool how? (conversions)struct Bool { // convert from pointers template <typename T> constexpr Bool(T * const x) noexcept : val { x!= nullptr } {} constexpr Bool(std::nullptr_t) noexcept {} // other conversion attempts are not allowed template <typename T, typename = std::enable_if_t< std::is_constructible<bool,T>::value && std::is_class_v<std::remove_cv_t<std::remove_reference_t<T>> >> > constexpr Bool(T const &x) noexcept :Bool(static_cast<bool>(x)){}

15 . 8

Page 78: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

not sure if this is overengineered… but I have tests

Speaker notes

Bool testing shortcut eval constexpr static Bool const t { true }; constexpr static Bool const f { false }; void OperatorOrShortCut() const { int i{}; t || ++i; // t converts to bool ASSERT_EQUAL(0,i); } void OperatorOrShortCutPass() const { int i{}; f || ++i; // f converts to bool ASSERT_EQUAL(1,i); }

15 . 9

Page 79: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Bool testing conversionstatic_assert(not BoolDoesConvertFrom_v<char>);static_assert(not BoolDoesConvertFrom_v<int>);static_assert(not BoolDoesConvertFrom_v<double>);static_assert(BoolDoesConvertFrom_v<bool>);static_assert(BoolDoesConvertFrom_v<int *>);static_assert(BoolDoesConvertFrom_v<std::nullptr_t>);

15 . 10

Using Bool for comparisontemplate <typename U, typename Bool=::pssst::Bool>struct Eq{ friend constexpr Bool operator==(U const &l, U const& r) noexcept { auto const &[vl]=l; auto const &[vr]=r; return Bool(vl == vr); } friend constexpr Bool operator!=(U const &l, U const& r) noexcept { return !(l==r); }};

15 . 11

Page 80: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Using Bool for relopstemplate <typename U, typename BOOL=bool>struct Order: Eq<U,BOOL> { friend constexpr BOOL operator<(U const &l, U const& r) noexcept { auto const &[vl]=l; auto const &[vr]=r; return BOOL(vl < vr); } friend constexpr BOOL operator>(U const &l, U const& r) noexcept { return r < l; } friend constexpr BOOL operator<=(U const &l, U const& r) noexcept { return !(r < l); } friend constexpr BOOL operator>=(U const &l, U const& r) noexcept { return !(l < r); }};

15 . 12

Bool does it really work?Unfortunately, a lot of code expects relational operators to

return just bool, and don’t do the explicit conversion anddo not return auto…

template<typename _Tp>struct less : public binary_function<_Tp, _Tp, bool>{ constexpr bool operator()(const _Tp& __x, const _Tp& __y) const { return __x < __y; } // error: cannot convert 'pssst::Bool' to

'bool' in return};

15 . 13

Page 81: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

What to do with Bool?

Decision keep bool result for comparisons andeliminate pssst::Bool for simplicity

currently, I think, it is a nice idea that doesn’t work well

@foonathan doesn’t use ts::boolean as comparisonresult type { style=“font-size:80%” }

Making pssst::Bool convert implicitly to bool kills itsinitial motivation

15 . 14

C++20 <=> 3-waycomparison

16 . 1

Page 82: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Almost Simplest ST (1)C++20: default comparison

equality (==) also implies inequality (!=)

must return bool, param as const& and be constbut duplication for every value class

struct literper100km { double value; constexpr bool operator==(literper100km const&) const noexcept = default; // C++20 defaulted equality};

16 . 2

However, C++20 provides a way to get comparison operators generated by the compiler with =default.Unfortunately, we must add the defaulted definition to each of our strong type structs. Another benefit of C++20 is,that a defined equality comparison directly implies availability of a defined inequality comparison (operator!=).

Speaker notes

Page 83: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

Almost Simplest ST (2)C++20: ' <=> all comparisons as defaults

defaulted spaceship also provides equality< <= > >= == !=

struct literper100km { double value; constexpr auto operator<=>(literper100km const &) const noexcept = default; // C++20 defaulted 3way comparison};//... ASSERT(consumption(literGas{40}, kmDriven{500}) < consumption(literGas{9}, kmDriven{110}));

16 . 3

Instead of equality comparison we can also default the 3way comparison operator (aka spaceship operator). If thespaceship operator is defined as =default then all 6 possible comparison operators are defined.

Q: Should we define comparison operators across different types, e.g., literGas and kmDriven ?

A: No, in general comparing apples and pears is not useful, because they represent different things.

Speaker notes

Page 84: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

operator<=> = default

still duplication for every value class

struct literGas { double value; constexpr auto operator<=>(literGas const &) const noexcept = default;};struct kmDriven { double value; constexpr auto operator<=>(kmDriven const &) const noexcept = default;};struct literper100km { double value; constexpr auto operator<=>(literper100km const &) const noexcept = default;};

16 . 4

Similar to default definitions of equality, the spaceship operator definintion has to be repeated for each of our strongtype classes.

Speaker notes

Page 85: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

CRTP + 3way comparison (1)Bad things with initialization and comparison:

struct literper100km: Out<literper100km>{ // CRTP Pattern double value; constexpr auto operator<=>(literper100km const &r) const noexcept = default;};//... ASSERT_EQUAL(literper100km({}, 8.1), consumption(consumed, distance));// can not use {} for init bc. macro // need to provide extra {} for base class (ctor can help)// error: operator== is implicitly deleted because the default // definition would be ill-formed

16 . 5

Bummer, while CRTP base classes seem to be a good idea in general, they break the ability to define a defaultedcomparison operator.

Speaker notes

Page 86: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

CRTP + 3way comparison (2)Let’s mix it in!

No luck with defaulting (really?).

template<typename U>struct Cmp { constexpr auto operator<=>(U const &) const noexcept = default;};// error: defaulted member 'operator<=>(const U&) const'// must have parameter type 'const

strong_with_struct_output_mixin::Cmp<U>&'

16 . 6

Unfortunately, defaulting the comparison operator via a CRTP base class also does not work, because the operatorparameter type must match the class type where it is defaulted.

Speaker notes

Page 87: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

CRTP + 3way comparison (3)Listen to the compiler?

Look extra braces for each base object

template<typename U>struct Cmp { friend constexpr auto operator<=>(Cmp<U> const &, Cmp<U> const &) noexcept =default;};struct literper100km: Cmp<literper100km>, Out<literper100km>{ double value;};void demonstrateStrongTypeProblem() { literGas consumed{{}, 40}; kmDriven distance{{}, 500}; ASSERT_EQUAL(literper100km({}, {}, 8.1), consumption(consumed, distance)); // should fail, but OK }

16 . 7

Well, if we listen to the compiler, we now have a class where different values are equal! Why? Because wecompare an empty base class in our comparison operator and not the class holding the actual value. Since empty,all instances of the base class are equal.

So defaulting comparison does not work when CRTP is involved!

Also we see another problem growing: For each base class we need to provide an extra pair of braces to initializeits subobject in aggregate initialization. We can only elide empty braces at the end of the initializer list, but not in thefront.

Speaker notes

Page 88: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

CRTP & 3way comparison (4)

implementing <=> only provides < <= > >= (

template<typename U> struct Cmp { friend constexpr auto operator<=>(U const &l, U const &r) noexcept { auto const &[vl] = l; auto const &[vr] = r; return vl <=> vr; }};struct literper100km: Cmp<literper100km>, Out<literper100km>{ double value;};void demonstrateStrongTypeProblem() { literGas l{{}, 40}; kmDriven km{{}, 500}; ASSERT_EQUAL(literper100km({},{},8.1),consumption(l, km)); // error: no match for operator== }

16 . 8

Another “Gotcha”: defining the 3 way comparison operator only provides us with the inequality operators and notequality operators. [class.compare.default p.5]

Speaker notes

Page 89: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

CRTP + 3way comparison (5)Must define equality comparison as well

implementing <=> only provides < <= > >= (

template<typename U>struct Cmp { friend constexpr auto operator<=>(U const &l, U const &r) noexcept { auto const &[vl] = l; auto const &[vr] = r; return vl <=> vr; } friend constexpr bool operator==(U const &l, U const &r) noexcept { auto const &[vl] = l; auto const &[vr] = r; return vl == vr; } // must be resurrected};

16 . 9

Therefore, we need to define the equality operator as well. As a result, mixing in comparison operators is not muchless tedious than using the regular set of operators in pre-C++20 terms.

Speaker notes

Page 90: Simpler Strong Types...Motivation Order of Argument Bug Prevention Communicate and Check Semantics of Values Limit Operations to Useful subset 4 . 3 Often, strong, user-definedtypes

C++20 comparison summarydefaulting == provides !=in class operator<=>() =default works

but not with empty base classes (CRTP mixins)Implementation of <=> loses ==== and != only provided when <=> = default

CRTP Mix-in of operator<=> needsimplementation

also of operator ==

auto operator<=>(T const&) = default;

16 . 10

Why aggregate?

https://godbolt.org/z/xWfnM5

godbolt.org example

16 . 11


Recommended