Don’t write exception classes, declare exception types

Some notes

Slideware
Slideware
Personal opinions
Slideware
Personal opinions
No universal solution here
Slideware
Personal opinions
No universal solution here

A bit of history

How might an average exception look like?

class bad_value : public std::exception
{
  std::string message;
public:
  explicit bad_value(const std::string& m)
   : message(m) {}

  const char* what() const noexcept override {
    return message.c_str();
  }
};
class bad_value : public std::exception
{
public:
  const char* what() const noexcept override {
    return "bad_value";
  }
};
class bad_value : public std::runtime_error
{
  using std::runtime_error::runtime_error ;
};
using bad_value =
        tagged_error<struct bad_value_tag> ;
template<typename T>
class tagged_error : public std::runtime_error
{
  using std::runtime_error::runtime_error ;
};
template<typename T>
class tagged_error : public std::runtime_error
{
  using std::runtime_error::runtime_error ;
};

using bad_value =
        tagged_error<struct bad_value_tag> ;

using bad_index =
        tagged_error<struct bad_index_tag> ;
throw bad_value{"bad_value: the problem"};

Lets look at something different

Numbers as types

template<int val>
struct Number {
};

using NumberOne = Number<1> ;
using NumberTwo = Number<2> ;
void print(const NumberOne&) {
    std::cout << "one\n" ;
}

void print(const NumberTwo&) {
    std::cout << "two\n" ;
}

int main(){
    print(NumberOne{});
    print(NumberTwo{});
}

Add some type category

template<typename T>
struct ErrorType {
  constexpr ErrorType(T val) noexcept: value{val}
  {};
  T value ;
};

template<typename T, T val>
struct Error final: ErrorType<T> {
   constexpr Error() noexcept: ErrorType<T>(val){};
};
enum PosixErrNo {
    /* NoError = 0, */
    PosixEPERM = 1, PosixENOENT = 2, PosixESRCH = 3
};

using ErrEPERM = Error<PosixErrNo, PosixEPERM>;
enum PosixErrNo {
    /* NoError = 0, */
    PosixEPERM = 1, PosixENOENT = 2, PosixESRCH = 3
};

using ErrEPERM = Error<PosixErrNo, PosixEPERM>;
// or, with some more syntax sugar

template<PosixErrNo V>
using PosixErrType = Error<PosixErrNo, V>;

using ErrENOENT = PosixErrType<PosixENOENT>;
template<typename T>
void printErrorType (const ErrorType<T>& et) {
    std::cout << et.value << std::endl ;
}

int main(){

    ErrEPERM  eperm{} ;
    printErrorType(eperm) ;

    constexpr ErrENOENT enoent ;
    static_assert(enoent.value == 2, "");
}

Short summary

Short summary

  • Numbers as type are sometimes useful

Short summary

  • Numbers as type are sometimes useful

  • Not just for errors

Short summary

  • Numbers as type are sometimes useful

  • Not just for errors

Don’t wrap OS error codes!

For OS error codes we have already

<system_error>

Something different

Often a log line like this is enough:

WhatHappend::File::Line::Function

C++20 will bring <source_location>

// untested since so far in no compiler

constexpr auto location =
    std::source_location::current();

std::cout << location.file_name() << ":"
          << location.line() << " "
            << location.function_name()

Fantasy code

template<typename T>
struct typed_location {

  static const char* type{typename(T)} ;

  source_location location;

  constexpr typed_location(source_location sl)
    :location(sl)
};

using bad_value_exception =
    typed_location<struct bad_value>;

typed_location

using bad_value_exception =
    typed_location<struct bad_value>;

throw bad_value_exception {
  std::source_location::current()
} ;

with a operator<< overload

produces exactly the log I want

bad_value::source.cpp::123::get_some_data

typed_location

Some problems
  • source location not here yet

  • compile-time typename does not exist at all

typed_location

Some problems
  • source location not here yet

  • compile-time typename does not exist at all

typename would be useful for a lot more,
e.g. serialization

typed_location

Here is a possible implementation

Source location

Until we can use the one from the standard

check your experimental folder
struct source_loation{
    const char* file;
    int line;
    const char* function;

    constexpr source_loation(const char* fl,
                             int l,
                            const char* fn)
    : file(fl), line(l), function(fn) {
    }
} ;

#define current_location \
  source_loation{__FILE__, __LINE__, __FUNCTION__}
using cstr = const char* const ;
constexpr bool equal(cstr a, cstr b) {
  return *a == *b &&
    (*a == '\0' || equal(a + 1, b + 1));
}

int main() {
    constexpr auto sl = current_location ;
    static_assert (sl.line == 28) ;
    static_assert (equal(sl.function, "main")) ;
}

Compile time typename

template <typename T>
constexpr auto type_name()
{
#if defined (_MSC_VER)
    return __FUNCSIG__ ;
#elif defined (__GNUC__) || defined (__clang__)
    return __PRETTY_FUNCTION__ ;
#else
    error ("compiler not supported, add a PR")
#endif
}
std::cout << type_name<struct Foo>();
std::cout << type_name<struct Foo>();

MSVC

auto __cdecl type_name<struct Foo>(void)

gcc/clang

auto type_name() [T = Foo]

The typename is there!

Miro

Let’s get the typename

template<typename T>
constexpr auto type_name() {
#if defined (_MSC_VER)
    constexpr cstr thisname=__FUNCSIG__;
    constexpr auto ang= first_c_in('<', thisname);
    constexpr auto space= first_c_in(' ', ang);
    constexpr auto tstart = (space == nullptr) ?
      next_c(ang) : next_c(space) ;
    constexpr auto tend=last_c_in('>', thisname);
#elif defined (__GNUC__) || defined (__clang__)
    constexpr cstr thisname=__PRETTY_FUNCTION__;
    constexpr auto eq= first_c_in('=', thisname);
    constexpr auto past_eq= next_c(eq);
    constexpr auto tstart=first_not(' ', past_eq);
    constexpr auto tend=last_c_in(']', thisname);
#else
    error ("compiler not supported, feel free to add a PR")
#endif
    constexpr auto len = tend - tstart ;
    carrier<len+1> tnc{} ;

    for (size_t i = 0 ; i < len; ++i){
      tnc.name[i] = *(tstart + i) ;
    }
    return tnc;
  template<std::size_t S> struct carrier {
    char name[S] = {0};

    constexpr const char* str() const {
      return &name[0];;
    }
  };

Bring it together

struct err_location {
  source_loation location ;
  const char*  error ;

  constexpr err_location(source_loation sl,
                          const char* e)
     : location{sl}, error{e} {}

};
template<typename T>
struct typed_location : public err_location {

  using name_t = decltype(sl::type_name<T>());
  static constexpr name_t
          tname = sl::type_name<T>();

  constexpr typed_location(source_loation sl)
    : err_location{sl, tname.str()} { }
};
std::ostream& operator<<(std::ostream& os,
                        const err_location& e)
{
    os << e.error << "::"
                    << e.location.file << "::"
                    << e.location.line << "::"
                    << e.location.function;
    return os;
}
using bad_value =
  typed_location<struct BadValue> ;

int main() {

  constexpr bad_value err {current_location} ;

  static_assert(sl::equal(err.error, "BadValue"),
                "unexpected typename") ;

  try {
    throw bad_value {current_location} ;

  } catch (const err_location& err) {
    std::cout << err << std::endl ;
  }
}

Short summary

Short summary

  • Location soon in the standard
    (output implementation defined)

Short summary

  • Location soon in the standard

  • type_name via function macros suboptimal

Short summary

Short summary

Short summary

Let’s hope for compile time reflections 2023

plus the time your project needs to add the required switches

:-(

Something different

What about the catch part?

No common base class

class my_type : public std::exception
{
public:
  const char* what() const noexcept override {
    return "my_type"; // + source location ....
  }
};

std::exception seems not optimal,

the info does not fit into const char* what() without copy compile time constants.

Also: C++ Core Guidelines

E.14: Use purpose-designed user-defined types as exceptions (not built-in types)

Reason A user-defined type is unlikely to clash with other people’s exceptions.

Assumption

The best way to handle exceptions is to create a log entry and then quit.

struct connectionError{} ;
struct sqlError{} ;
struct connectionError{} ;
struct sqlError{} ;


  try {
    throw sqlError() ;

  } catch (...) {
    log(std::current_exception()) ;
    std::exit(1) ;

  }
void log(std::exception_ptr problem) {
  assert(problem);
  try {
    std::rethrow_exception(problem);

  } catch(const connectionError&) {
    std::cout<< "connectioError \n" ;

  } catch(const sqlError&) {
    std::cout<< "sqlError \n" ;

  }
}

Short summary

  • Sometimes std::error not the best base class

Short summary

  • Sometimes std::error not the best base class

  • std::current_exception() and std::exception_ptr to put exception handling on one place

Short summary

  • Sometimes std::error not the best base class

  • std::current_exception() and std::exception_ptr to put exception handling on one place

  • Wish for the future
    Pattern matching, also for exception catching
    plus review of scope handling

Something different

What will this print?

int main() {
  std::cout << type_name<struct Foo>() ;
}
  • Foo

  • main::Foo

  • main()::Foo

  • something different

Implementation defined

Hopefully static reflections will fix this,

... if they will ever come

Now …​.

I wanted to complain

that this works

using bad_value =
typed_location<'b','a','d',' ','v','a','l','u','e'>;

but this not

  using bad_value =
        typed_location<"bad value"> ;

That we have to use macros

  using bad_value =
        typed_location<CTS("bad value")> ;

To make this working

template<const char... Chars>
struct typed_location : public err_location {

  static constexpr
  const char text[sizeof...(Chars)]{Chars...} ;

  constexpr typed_location(source_loation sl)
    : err_location{sl, &text[0]} { }
};
#define MAX_CONST_CHAR 100
#define MIN(a,b) (b)<(a)?(b):(a)

#define CTS(s)\
getChr(s,0),\
getChr(s,1),\
... continue up to \
getChr(s,99),\
getChr(s,100)

#define getChr(name, ii) ( \
  (MIN(ii,MAX_CONST_CHAR)) \
  <sizeof(name)/sizeof(*name)?name[ii]:0 \
  )

Or macro libraries like

using sad_value =
    typed_location<CTS("sad value")> ;

using bad_value =
    typed_location<CTS("bad value")> ;

void f(sad_value) {
    printf("sad");
}
void f(bad_value) {
    printf("bad");
}
int main()
{
  constexpr bad_value err{current_location} ;
  static_assert(equal(err.error, "bad value"), "");

  f(err) ;
  f(sad_value{current_location}) ;

  using bad_val2=typed_location<CTS("bad value")>;
  using std::is_same_v ;
  static_assert(is_same_v<bad_val2, bad_value>,"");
}

That we have half solutions like this

template <char... chars>
using tstr = std::integer_sequence<char, chars...>;

template <typename T, T... chars>
constexpr tstr<chars...> operator""_tstr() {
  return { };
}
template <typename T>
struct typed_location;

template<char... Chars>
struct typed_location<tstr<Chars...>>
  : public err_location {

  static constexpr const char
    text[sizeof...(Chars)+1]{Chars...,'\0'};

  constexpr typed_location(source_loation sl)
    : err_location{sl, &text[0]} { }
};
using sad_value =
    typed_location<decltype("sad value"_tstr)> ;

using bad_value =
    typed_location<decltype("bad value"_tstr)> ;

void f(sad_value) {
    printf("sad");
}
void f(bad_value) {
    printf("bad");
}
int main()
{
  constexpr bad_value err{current_location} ;
  static_assert(equal(err.error, "bad value"), "");

  f(err) ;
  f(sad_value{current_location}) ;

  using bad_val2=
    typed_location<decltype("bad value"_tstr)>;
  using std::is_same_v ;
  static_assert(is_same_v<bad_val2, bad_value>,"");
}

only one small detail:

warning:
string literal operator templates
are a GNU extension
      [-Wgnu-string-literal-operator-template]
constexpr tstr<chars...> operator""_tstr() {

C++20 will fix this!

Not just numbers as types

All user defined types (with no private members)

Also char arrays !

template <std::size_t N>
struct wrapper  {
  char val[N];
  constexpr wrapper(char const* src) :val{} {
    for (std::size_t i = 0; i < N; ++i)
      val[i] = src[i];
  }
};

template <std::size_t N>
wrapper(char const(&)[N]) -> wrapper<N>;
template <wrapper X> struct foo {
    constexpr const char* str() {
        return &X.val[0] ;
    }
};

using f1 = foo<"hello">;
using f2 = foo<"bello">;

void fun(f1 f) { std::cout << f.str()  ; }
void fun(f2 f) { std::cout << f.str()  ; }
int main()
{
  static_assert(equal(f1{}.str(), "hello")) ;
  static_assert(equal(f2{}.str(), "bello")) ;
  fun(f1{});
  fun(f2{});
}

This is nice!

Overall summary

Start thinking about boilerplate can lead to interesting journeys

Overall summary

Start thinking about boilerplate can lead to interesting journeys

some of them more useful than others

:-)

  • Generic programming

  • Compile time programming

  • New, upcoming and missing features

  • Some nice code games

Exceptions is a complex topic

as promised, no general solution

3 exception worlds

2 'official', 1 in-official

  • Exceptions

  • Error-codes

  • others (expected/outcome)

p0709

Lets hope for a better future

zero-overhead deterministic exceptions

Thanks for listening!