0% found this document useful (0 votes)
77 views

The C++ Type System Is Your Friend 2016

Uploaded by

jaansegus
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
77 views

The C++ Type System Is Your Friend 2016

Uploaded by

jaansegus
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 39

The C++ Type System is your

Friend

ACCU Oxford, October 2016

Hubert Matthews
[email protected]
Why this talk?

Copyright © 2016 Oxyware Ltd 2/39


Safe, performant, reusable code
• General desiderata (in priority order):
• We want our code to help us prevent
avoidable mistakes, preferably at compile
time
• We want the run-time cost of this safety to be
zero compared to unsafe coding (or people
will avoid doing it)
• We want the code to be reusable and generic
(i.e. a library) so we can avoid having to
reimplement it every time

Copyright © 2016 Oxyware Ltd 3/39


(Mostly) typeless programming
• Assembler
– Integer can be used as an address and vice versa
– Machine efficiency at the cost of programmer effort
– Translate into the language – domain knowledge is
embedded, not obvious or easy to decipher
– Liberal use of comments (hopefully!)
– High maintenance cost
• B, BCPL
– Hardly any type safety
– 3 * (4 + 5) gives the value 27
– 3 (4 + 5) calls function at address 3 with value 9
• C preprocessor
– Programming with strings

Copyright © 2016 Oxyware Ltd 4/39


Machine-typed programming
• C and primitive-based C++
– Avoids the type puns and mistakes of assembler
– High machine efficiency
– Better programmer efficiency
– Uses the underlying machine types (int, float,
typed pointers)
– Adds structures and aggregates
– Abstraction through files
– Still have to translate domain into a program
– Little opportunity for compile-time checking or
proofs

Copyright © 2016 Oxyware Ltd 5/39


Type-rich programming
• Higher-level C++
– Uses the C++ type system extensively to create
lightweight abstractions that increase the amount
of domain knowledge in the program without
sacrificing machine efficiency
– The type system is a proof system – 100% compile-
time checking if a construct is illegal
– Well used, it can make code safer and more
reusable
– Stroustrup is a big fan of this approach

Copyright © 2016 Oxyware Ltd 6/39


Little white lies:
C structs, C++ RTTI
The miracle of compilation
Run time Compile time

C++ Machine Language Application


types types types

C Machine Language
types types what this talk is
focused on

asm Machine thrown away at run time


types (no memory or CPU
overhead)

Copyright © 2016 Oxyware Ltd 7/39


Primitive or typed API
// Is this y/m/d (ISO), d/m/y (European) or m/d/y (broken US)?

Date(int, int, int);

// Unambiguous and expressive

Date(Year, Month, Day);

// Helps with expressivity but not correctness as it's just a


// aliased type

using Year = int; // just a type alias

// We need a completely separate type to get safety as well

class Day { /*...*/ };

• Creating separate types for values catches type


errors at compile time
Copyright © 2016 Oxyware Ltd 8/39
Physical types
typedef float meters, seconds;

meters m = 3.4;
seconds s = 1.5;
auto velocity = m + s; // oops, probably meant / not +
// but it still compiles

typedef float feet;

feet f = 5.6;
meters m2 = m + f; // physical units correct but
// measurement system wrong

// Mars Climate Orbiter crashed because of a pound-seconds


// and newton-seconds mismatch (1999)

• Lots of possibilities for simple errors that are


hard to find and debug but easy to prevent
Copyright © 2016 Oxyware Ltd 9/39
Whole Value pattern explicit c/tr to
avoid automatic
conversions
class Year {
public:
explicit Year(int y) : yr(y) {} user-defined
operator int() const { return yr; }
private: conversion is safe:
int yr; narrow to wide
};

Year operator”” _yr(unsigned long long v) { return Year(v); }

Year y = 2016_yr;

• Holds a value but has no operations – all operations


done on the base type (int, here) through widening
conversion
• Safe way to pass values but not foolproof
• Repetitive when defining multiple types
Copyright © 2016 Oxyware Ltd 10/39
Templates to the rescue
enum class UnitType { yearType, monthType, dayType };

template <UnitType U>


class Unit {
public:
explicit Unit(int v) : value(v) {}
operator int() const { return value; }
private:
int value;
};

using Year = Unit<UnitType::yearType>;


using Month = Unit<UnitType::monthType>;
using Day = Unit<UnitType::dayType>;

Date(Year, Month, Day); // now type-safe API

• Removes repetition across types


• As efficient as primitives; functions are inlined

Copyright © 2016 Oxyware Ltd 11/39


Adding checking of values
template <UnitType U, int low, int high>
class Unit {
public:
constexpr explicit Unit(int v) : value(v) {
if (v < low || v > high) throw std::invalid_argument(“oops”);
}
constexpr operator int() const { return value; }
private:
int value;
};

using Year = Unit<UnitType::yearType, 1900, 2100>;

Year tooSmall(1000); // throws at run-time


constexpr Year tooBig(2300); // compile-time error

• Extra checking for types can be added for both


run-time and compile-type checking
• Constexpr is very powerful keyword for this
Copyright © 2016 Oxyware Ltd 12/39
Operations
• Up to now we have used conversions to allow
us to operate on our types, which is simple but
possibly error-prone as we can't control what
operations are valid (we get everything that int
can do)
• Essentially our types are just labels
• Let's add operations and remove the
conversion (or make it explicit)

Copyright © 2016 Oxyware Ltd 13/39


Please imagine all functions are
Operations constexpr – it makes the slides shorter!

template <UnitType U, int low, int high>


class Unit {
public:
constexpr explicit Unit(int v) : value(v) {
if (v < low || v > high) throw std::invalid_argument(“oops”);
}
constexpr explicit operator int() const { return value; }
private:
int value;
};

Year operator+(Year yr, int i) { return Year(int(yr)+i); }


Year operator+(int i, Year yr) { return Year(int(yr)+i); }

// define only those operations that make


// sense in the domain for a given type

• Year+Year doesn't make sense but Year+int


does, as does Year-Year
Copyright © 2016 Oxyware Ltd 14/39
Operations
Type Desirable operations Non-sensical operations

Date Date+int => Date Date * int


int+Date => Date
Date-Date => int
Date-int => Date
Date < Date => bool
Date == Date => bool

Money Money * float => Money Money + float


Money / float => Money Money – float
Money < Money => bool
Money == Money => bool

• Every type has its own set of operations


• How to make this generic?
• How do we avoid repetitive boilerplate code?
Copyright © 2016 Oxyware Ltd 15/39
Reuse through client libraries
bool operator==(Year y1, Year y2) { return int(y1) == int(y2); }
bool operator<(Year y1, Year y2) { return int(y1) < int(y2); }

#include <utility>
using namespace std::rel_ops; // defines <=,>=,>,!=

// namespace std { namespace rel_ops {


// template <typename T>
// bool operator>(T t1, T t2) { return t2 < t1; }
// }}

bool ge = y1 >= y2;

• Can't be used in the definition of a class


• Client has to decide to use these broad templates
• Only handles relational operators

Copyright © 2016 Oxyware Ltd 16/39


Reuse through inheritance – CRTP
template <typename Derived>
class Ordered { downcast to
public:
const Derived & derived() const { Derived is safe
return static_cast<const Derived &>(*this);
}
bool operator>(const Ordered & rhs) const {
return rhs.derived() < derived(); CRTP pattern:
} deriving from a
}; template using
class Year : public Ordered<Year> {
yourself!
public:
explicit Year(int i) : val(i) {}
bool operator<(const Year & rhs) const { return val < rhs.val; }
private:
int val;
};

int main() {
Year y1(7), y2(5);
assert(y1 > y2); // true
}

Copyright © 2016 Oxyware Ltd 17/39


Reuse through inheritance – CRTP
• The cast in Ordered::derived() is checked at
compile-time as it's a static_cast
• There is no overhead in terms of space or time
• All calls are resolved at compile time
• Compile-type polymorphism
• Using a virtual call instead would mean:

Larger class (vtable pointer)

Run-time dispatch (virtual call)

Can't be constexpr (forces run-time eval)

Probably not inlined
• Very common technique in libraries like Boost
Copyright © 2016 Oxyware Ltd 18/39
Physical
template <typename V, UnitSys U, int M, int L, int T>
class Quantity { quantities
public:
explicit Quantity(V v) : val(v) {}
explicit operator V() const { return val; }
private:
V val;
};

template <typename V, UnitSys U, int M, int L, int T>


auto operator+(Quantity<V, U, M, L, T> q1, Quantity<V, U, M, L, T> q2) {
return Quantity<V, U, M, L, T>(V(q1) + V(q2));
}

template <typename V, UnitSys U,


int M1, int L1, int T1, int M2, int L2, int T2>
auto operator/(Quantity<V, U, M1, L1, T1> q1,
Quantity<V, U, M2, L2, T>2 q2) {
return Quantity<V, U, M1-M2, L1-L2, T1-T2>(V(q1) / V(q2));
}

using meters = Quantity<float, SIUnits, 0, 1, 0>;


using seconds = Quantity<float, SIUnits, 0, 0, 1>;

int main() {
auto velocity = 23.1_meters / 1.5_secs;
• A
// auto error = 23.1_meters + 1.5_secs; // compile-time error
}
Copyright © 2016 Oxyware Ltd 19/39
Physical quantities and dimensions
• Allows us to define operations that convert
types (here the dimension exponents are
calculated to give new dimension values)
• Prevents physically impossible calculations
• Prevents mixing of measurement units (e.g.
mixing SI Units and imperial units)
• Can be used for related “flavours” of types,
such as multiple currencies that are “the same
underlying thing” but with different units

Copyright © 2016 Oxyware Ltd 20/39


Compile-time reflection
template <typename V, UnitSys U, int M, int L, int T>
class Quantity {
public:
using value_type = V;
republish
static constexpr UnitSys unit_sys = U; template
static constexpr int mass_exponent = M; parameters
static constexpr int length_exponent = L;
static constexpr int time_exponent = T;
explicit Quantity(V v) : val(v) {}
explicit operator V() const { return val; } create a compatible
private: type using
V val; reflection
};

using length = Quantity<float, SIUnits, 0, 1, 0>;


using time = Quantity<length::value_type, length::unit_sys, 0, 0, 1>;

template <typename V, UnitSys U>


using Mass = Quantity<V, U, 1, 0, 0>;
if-statement based
template <typename Q> on constants will
void print_units(Q q) { be removed
• A
if (Q::unit_sys == UnitSys::SIUnits)
std::cout << “Using SI units\n”;
}
Copyright © 2016 Oxyware Ltd 21/39
Tailoring operations – library code
template <typename T>
struct op_traits {
static constexpr bool add_scalar = false;
static constexpr bool add_value = false;
};

template <typename T, typename Requires =


std::enable_if_t<op_traits<T>::add_scalar>>
auto operator+(T t, int i)
{
return T{t.val+i};
}

template <typename T, typename Requires =


std::enable_if_t<op_traits<T>::add_value>>
auto operator+(T t1, T t2)
{
return T{t1.val+t2.val};
}

// same for operator+(int i, T t);

Copyright © 2016 Oxyware Ltd 22/39


Tailoring operations – client code
struct Year { int val; };

template <>
struct op_traits<Year> {
static constexpr bool add_scalar = true;
};

int main() {
Year y1{10}, y2{5};
//auto y3 = y1 + y2; // compiler error
auto y3 = y1 + 2;
}

• Library user defines what operations from the


library are valid by setting the appropriate
traits
Copyright © 2016 Oxyware Ltd 23/39
Where are we now?
• Let's look at the generated code for an example
that puts all of these things together to see
how efficient it is (both code and data)
• Constexpr and user-defined literals
• Physical dimensions and unit types
• CRTP for operator inheritance

int main() // generated code


{ // g++ -O3
Distance d1 = 5.2_meters;
Distance d2 = 4.6_meters; movl $5, %eax
Time t = 2.0_secs; ret
auto v = (d1+d2+Distance(d1 > d2)) / t;
return int(float(v)); // return 5;
}

Copyright © 2016 Oxyware Ltd 24/39


Templates and policies
• Another example: fixed-length strings that
prevent the sort of basic buffer overflow bugs
that traditionally haunt C programs

template <size_t N>


class FixedString {
public: truncates the
static constexpr max_size = N;
explicit FixedString(const char * p = “”) { incoming string:
strncpy(data, p, N); this is a policy
data[N-1] = 0; decision
}
size_t size() const { return strlen(data); }
private:
char data[N];
};

Copyright © 2016 Oxyware Ltd 25/39


Templates and policies
• This class truncates its input. This may be
what you want, but there are other options:
• Add an entry to the diagnostic log and
continue (if overflow is expected and OK)
• Throw an exception (if overflow shouldn't
happen)
• Reboot the system (if overflow is a serious
error)
• Dump a stack track and jump into the
debugger (during development and test)

Copyright © 2016 Oxyware Ltd 26/39


Implementing policies
• Let's use a policy on overflow
struct Complain {
static void overflow(size_t n, const char * s, const char * p) {
std::cout << "Overflow of FixedString<" << n << "> and "
"contents " << s << " when adding " << p << std::endl;
}
};

template <size_t N, typename OverflowPolicy = Complain>


class FixedString {
public:
constexpr explicit FixedString(const char * p = "") {
char * s = data;
while (s-data != N-1 && (*s++ = *p++)) {}
if (*(p-1) != 0) OverflowPolicy::overflow(N, data, p);
*s = 0;
}
FixedString<8>
FixedString<8> fs1(“hello”);
fs1(“hello”); //
// no
no overflow
overflow
FixedString<5> fs2(“hello”);
FixedString<5> fs2(“hello”); //
// prints msg
prints msg
private:
char data[N];
template
template <size_t
<size_t N>
N>
};
using
using NoisyString == FixedString<N,
NoisyString FixedString<N, ResetOnOverflow>;
ResetOnOverflow>;
Copyright © 2016 Oxyware Ltd 27/39
Comparing policies and CRTP
• CRTP has to use a compile-time downcast to
access the derived class' functionality (i.e. to
get itself “mixed in”)
• CRTP is usually used for injecting library
functionality

• Policies don't need a downcast as they are a


pure “up call” to a static function
• Policies are useful for parametrising rules and
validation logic (such as in constructors)

Copyright © 2016 Oxyware Ltd 28/39


Constructor validation logic
• Let's use a policy to enforce that quantities are
non-negative

struct NonNegChecker {
constexpr NonNegChecker(float f) {
if (f < 0) throw std::invalid_argument("oops!");
}
};

template <UnitType U, int M, int L, int T, class CtrCheck=NonNegChecker>


class Quantity : public Ordered<Quantity<U, M, L, T>>, public CtrCheck {
public:
constexpr explicit Quantity(float v) : CtrCheck(v), val(v) {}
constexpr explicit operator float() const { return val; }
bool operator<(Quantity other) const { return val < other.val; }
private:
float val;
};

Copyright © 2016 Oxyware Ltd 29/39


Constexpr constructor check
• Constexpr in effect interprets your code at
compile time using a cut-down version of the
compiler
• C++11 version is limited, C++14 is general
• Some limitations
• Can't initialise the string directly
• If the CtrCheck constructor doesn't complete
correctly because an exception has been
thrown then this becomes a compiler error
• If it doesn't throw then no code is generated
for CtrCheck
Copyright © 2016 Oxyware Ltd 30/39
Effect of constructor validation logic
• So, what about the generated code?

constexpr Distance d0 = -1.1_meters; (compiler error)

Distance d0 = -1.1_meters; (throws at runtime)

int main() // generated code


{ // g++ -O3
Distance d1 = 5.2_meters;
Distance d2 = 4.6_meters; movl $5, %eax
Time t = 2.0_secs; ret
auto v = (d1+d2+Distance(d1 > d2)) / t;
return int(float(v));
}
same as
before

Copyright © 2016 Oxyware Ltd 31/39


Implicit v. explicit interfaces – DSELs

Implicit interface Explicit interface


All operations Only defined operations
Zero work to create Requires work to create
Easy to misuse Hard to misuse
Little checking Lots of checking
Wider than necessary Only as wide as necessary

Resource management
Destructors Most operations
Cleanup should be explicit
Rollback

Invisible for safety Visible, so safe

• A domain-specific language embedded inside C++


• Our configuration interface is mostly template-based
Copyright © 2016 Oxyware Ltd 32/39
Templates and “concepts lite” time

Client
OO planned
Interface
whitelist
Server

templates Tmpl<T>
Tmpl<X> ad hoc
concepts
X

• “Concepts Lite” adds type checking to template arguments


• Better error messages, same or better compilation speed
• G++ 6 has concepts – let’s try them out!
Copyright © 2016 Oxyware Ltd 33/39
Concepts code – first attempt
template <typename T>
concept bool HasOpLessThan() {
return requires(T t1, T t2) { t1 < t2; };
}

template <HasOpLessThan Derived> constrain


class Ordered template
public: argument
const Derived & derived() const {
return static_cast<const Derived &>(*this);
}
bool operator>(const Ordered & rhs) const {
return rhs.derived() < derived();
}
};

// Ordered<Year> doesn’t compile because Year is


// an incomplete type at this point – hmmm...

class Year : public Ordered<Year> { … };

Copyright © 2016 Oxyware Ltd 34/39


Concepts code – working code
template <typename T>
concept bool HasOpLessThan() {
return requires(T t1, T t2) { t1 < t2; };
}
unconstrained
template <typename Derived> template
class Ordered
public: argument
const Derived & derived() const {
return static_cast<const Derived &>(*this);
}
bool operator>(const Ordered & rhs) const constrain
requires HasOpLessThan<Derived>() member
{ function
return rhs.derived() < derived();
} instead
};

// Ordered<Year> now compiles because Year is


// a complete type at this point – yay!

class Year : public Ordered<Year> { … };

Copyright © 2016 Oxyware Ltd 35/39


Error messages without concepts
struct X : Ordered<X> {};
const bool x = X() > X();

conc_ordered.cpp: In instantiation of ‘bool


Ordered<Derived>::operator>(const Ordered<Derived>&) [with Derived
= X]’:
conc_ordered.cpp:31:24: required from here
conc_ordered.cpp:18:30: error: no match for ‘operator<’ (operand
types are ‘const X’ and ‘const X’)
return rhs.derived() < derived();

Copyright © 2016 Oxyware Ltd 36/39


Error messages with concepts
struct X : Ordered<X> {};
const bool x = X() > X();

conc_ordered.cpp:31:20: error: no match for ‘operator>’ (operand


types are ‘X’ and ‘X’)
const bool x = X() > X();
~~~~^~~~~
conc_ordered.cpp:16:10: note: candidate: bool
Ordered<Derived>::operator>(const Ordered<Derived>&) requires
(HasOpLessThan<Derived>)() [with Derived = X]
bool operator>(const Ordered & rhs) requires
HasOpLessThan<Derived>() {
^~~~~~~~
conc_ordered.cpp:16:10: note: constraints not satisfied
conc_ordered.cpp:4:14: note: within ‘template<class T> concept
bool HasOpLessThan() [with T = X]’
concept bool HasOpLessThan() {
^~~~~~~~~~~~~
conc_ordered.cpp:4:14: note: with ‘X t1’
conc_ordered.cpp:4:14: note: with ‘X t2’
conc_ordered.cpp:4:14: note: the required expression ‘(t1 < t2)’
would be ill-formed

Copyright © 2016 Oxyware Ltd 37/39


FixedString with and without concepts
struct X {};
FixedString<4, X> x;

FixedString.cpp: In instantiation of ‘constexpr FixedString<N,


OverflowPolicy>::FixedString(const char*) [with long unsigned int
N = 4ul; OverflowPolicy = X]’:
FixedString.cpp:79:23: required from here
FixedString.cpp:40:37: error: ‘overflow’ is not a member of ‘X’
OverflowPolicy::overflow(N, data, p);
~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~

FixedString.cpp: In function ‘int main()’:


FixedString.cpp:79:21: error: template constraint failure
FixedString<4, X> x;
^
FixedString.cpp:79:21: note: constraints not satisfied
FixedString.cpp:6:14: note: within ‘template<class T> concept bool
HasOverflow() [with T = X]’
concept bool HasOverflow() {
^~~~~~~~~~~
FixedString.cpp:6:14: note: the required expression ‘T::
overflow(0, "", "")’ would be ill-formed

Copyright © 2016 Oxyware Ltd 38/39


Summary
• Defining lightweight domain abstractions
allows us to have safer code with more
domain knowledge embedded in the code
• Zero or small runtime overhead in terms of
CPU or memory
• Can create reusable domain-specific libraries

(Disclaimer: There is no guarantee your programs


will end up being only a single instruction when
using these techniques)
Copyright © 2016 Oxyware Ltd 39/39

You might also like