4

I have some utility code that I've been using for years to safely call the ctype family of functions, it looks like this:

template<int (&F)(int)>
int safe_ctype(unsigned char c) {
    return F(c);
}

And is used like this:

int r = safe_ctype<std::isspace>(ch);

The idea being that it handles the need to cast the input int to an unsigned value for you in order to prevent undefined behavior. The specifics of this function is somewhat irrelivant though. Here's my question:

Now that in C++17 and later, noexcept is part of the type system, this is a compile error! Because all of the ctype functions are now noexcept.


EDIT: The above sentence is incorrect. the ctype family of functions are not noexcept. I was however getting a compiler error in gcc < 11.2. https://godbolt.org/z/cTq94q5xE

The code works as expected (despite being technically not allowed due to these functions not being addressable) with the latest versions of all 3 major compilers.


I can of course change my function to look like this:

template<int (&F)(int) noexcept>
int safe_ctype(unsigned char c) noexcept {
    return F(c);
}

But now it doesn't work when compiled as C++11 or C++14. So I end up having to do something like this:

#if __cplusplus >= 201703L
template<int (&F)(int) noexcept>
int safe_ctype(unsigned char c) noexcept {
    return F(c);
}
#else
template<int (&F)(int)>
int safe_ctype(unsigned char c) {
    return F(c);
}
#endif

Which is getting increasingly complex for such a simple task. So is there a way to make the function pointer:

  1. valid for C++11 - C++20
  2. Accept both noexcept and non-noexcept when in C++17+

?

I tried doing something like this:

template<class F>
int safe_ctype(unsigned char c) noexcept {
    return F(c);
}

In the hopes that it would accept "anything", but sadly, no go.

Thoughts?

Evan Teran
  • 87,561
  • 32
  • 179
  • 238
  • 1
    It would be `template`, but C++17 only. – Jarod42 Feb 02 '22 at 15:37
  • 2
    FWIW, your original code has as least unspecified behavior: https://stackoverflow.com/questions/55687044/can-i-take-the-address-of-a-function-defined-in-standard-library – NathanOliver Feb 02 '22 at 15:40
  • 2
    I can’t reproduce this issue: while taking a reference to most standard-library functions is verboten as of C++20, taking a potentially-throwing reference to a non-throwing function is fine. – Davis Herring Feb 02 '22 at 15:55
  • 1
    "*Because all of the ctype functions are now noexcept.*" No, they're not. They *may* be `noexcept`, but they aren't *required* to be. – Nicol Bolas Feb 02 '22 at 15:56
  • @nicolbolas, sure they aren't required to be. But in clang and gcc, they appear to be and that is causing compile errors in c++17 mode and later. – Evan Teran Feb 02 '22 at 16:22
  • @EvanTeran: [No, they don't.](https://gcc.godbolt.org/z/1ajGEzM3n) They work just fine. – Nicol Bolas Feb 02 '22 at 16:23
  • @NicolBolas It appears you are right. The thing that I guess is confusing me is that maybe I was hitting a compiler bug in gcc < 11.2? https://godbolt.org/z/cTq94q5xE Which breaks... but not if I add the `noexcept` to the function reference. – Evan Teran Feb 02 '22 at 16:49
  • @NathanOliver Interesting! That is a detail that I never knew about. Thanks! (Sadly, that means I'll probably have to come up with an alternative solution :-/ ) – Evan Teran Feb 02 '22 at 17:09

2 Answers2

7

Now that in C++17 and later, noexcept is part of the type system, this is a compile error! Because all of the ctype functions are now noexcept.

It is not a compile error. Pointers to noexcept functions are implicitly convertible to pointers to potentially throwing functions, and thus the template accepting a pointer to potentially throwing functions works with both potentially throwing and noexcept functions. Only caveat is that the noexceptedness information is lost and might not be used for optimisation purposes.

Hence, the original solution satisfies both points 1. and 2.


Another problem pointed out in the comments is that the standard library functions (std::isspace) that you intend to use are not designated "addressable". Hence the behaviour of the program is unspecified (possibly ill-formed) due to forming a pointer to them.

To wrap such callable, you could use a lambda instead of a function pointer. But that makes the template itself obsolete since you can change the argument type of the lambda directly:

auto safe_isspace = [](unsigned char c){ return std::isspace(c); };
int r = safe_isspace(ch);

Though we no longer need to pass this into a template, so the same can be achieved with a plain function:

int // or bool?
safe_isspace(unsigned char c) noexcept // ...

Since this involves a bit of identical boilerplate for multiple functions, this is a good candidate for meta-programming.

eerorika
  • 232,697
  • 12
  • 197
  • 326
  • Why use a lambda instead of a regular function declaration: `auto safe_isspace(unsigned char c) {return std::isspace(c);}`? It'd be shorter. And you wouldn't miss the semicolon you forgot ;) – Nicol Bolas Feb 02 '22 at 16:11
  • Good answer! The non-addressable functions in the standard library have tripped me up more than once. They are non-addressable because they **may** (*allowed implementation detail*) be implemented as emitted intrinsics rather than the more familiar addressable functions. And unlike inline functions, they don't have an addressable fallback. – Eljay Feb 02 '22 at 16:14
  • @NicolBolas Because the thought process was to replace a function pointer of the template. But indeed, since the function template itself is obsolete, the source of the train of thought has disappeared and we might as well use a function. I was in fact writing a next paragraph about using just a function :) – eerorika Feb 02 '22 at 16:14
  • Thanks, I was being confused by the fact that my code breaks in gcc < 11.2, I didn't realize that it works as expected in the latest release. Now I just need to fix the fact that I'm apparently not technically supposed to take a function pointer/reference to these functions :-/ – Evan Teran Feb 02 '22 at 16:52
4

Because all of the ctype functions are now noexcept.

This is untrue. C++17 did not add noexcept to any C-library functions accessed through the C++ c* headers. You can see here that all of the C++ function declarations do not contain noexcept. And a standard library implementation is not allowed to make non-noexcept functions noexcept.

Secondly, even if it were noexcept, a noexcept function pointer can be converted into a throwing function pointer (but not the other way around). So your code compiles.

But most importantly, C++20 makes it clear that you are not allowed to get function pointers for any C++ standard library function unless it is specifically stated to be "addressable". And there are very few addressable functions in the C++ standard library.

So in C++20, your code will yield UB. You're just going to have to write wrappers for the cctype functions if you want your code to work across all language versions.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982