How to fail a consteval function?

1.5k views Asked by At

I have the following function:

template <size_t TSize>
consteval size_t indexOf(SomeEnum someEnum,
                         const std::array<SomeEnum, TSize> &arr) {
  for (size_t i = 0; i < TSize; ++i) {
    if (arr[i] == someEnum) {
      return i;
    }
  }
  // How to fail here?
  return SOME_DEFAULT_WRONG_VALUE;
}

The function should fail instead of returning a default value, but I can't throw an exception or call assert. I can add a static_assert to every call to the function (with a macro it will be less horrible), but I'd prefer a solution that works in the function. Is there a way to trigger a compilation failure in such a scenario?

3

There are 3 answers

3
Jarod42 On BEST ANSWER

You might simply omit the return

template <size_t TSize>
consteval size_t indexOf(SomeEnum someEnum,
                         const std::array<SomeEnum, TSize> &arr) {
  for (size_t i = 0; i < TSize; ++i) {
    if (arr[i] == someEnum) {
      return i;
    }
  }
}

Demo (clang warns though about that method).

Compiler would reject code reaching that path.

Demo

throw exception seems cleaner:

template <size_t TSize>
consteval size_t indexOf(SomeEnum someEnum,
                         const std::array<SomeEnum, TSize> &arr) {
  for (size_t i = 0; i < TSize; ++i) {
    if (arr[i] == someEnum) {
      return i;
    }
  }
  throw 42; // or more meaningful exception
}

Demo

1
Nicol Bolas On

You should avoid working with indices and instead use std::find which is constexpr now. If you want an index, you can just use pointer arithmetic subtraction from the begining of the array to compute the index.

However, if you can't do that, then just return TSize; it should act like the end iterator.

2
Barry On

Is there a way to trigger a compilation failure in such a scenario?

If the goal is to trigger a compilation failure, then the easiest thing to do is to throw an exception. Doesn't matter what the exception is, since it won't actually be thrown, it's the act of throwing an exception that will trigger a compile error because throwing is not allowed at constant evaluation time:

template <size_t TSize>
consteval size_t indexOf(SomeEnum someEnum,
                         const std::array<SomeEnum, TSize> &arr) {
  for (size_t i = 0; i < TSize; ++i) {
    if (arr[i] == someEnum) {
      return i;
    }
  }

  throw "failed to find someEnum";
}

If you want to be more explicit, you can just have a non-constexpr function without a definition:

void trigger_consteval_failure(char const*);

template <size_t TSize>
consteval size_t indexOf(SomeEnum someEnum,
                         const std::array<SomeEnum, TSize> &arr) {
  for (size_t i = 0; i < TSize; ++i) {
    if (arr[i] == someEnum) {
      return i;
    }
  }

  trigger_consteval_failure("failed to find someEnum");
}

In both cases, if you're looking for a value that is in the array, invoking this function is a valid constant expression. But if the index is not found, then we end up doing something that's now allowed in constant expressions and that's a hard compile error regardless, as desired.

It'd be nice if we could produce a better stack trace in this case, but I don't think there's actually a way to do that.