How to fail a consteval function?
Asked Answered
V

3

5

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?

Viscounty answered 29/4, 2021 at 15:26 Comment(17)
Generally these kinds of functions don't fail if they can't find the data, they return an out of bounds marker (usually -1). It's perfectly valid to search for something that doesn't exist in your collection.Pepin
Why can't you throw an exception or use assert?Isomerous
@Isomerous consteval.Amyloid
@chris: You can't use assert in a constant expression context.Narrate
You can still throw from a constexpr. I'm not familiar enough with consteval to be certain, but I believe you should be able to throw from them as well.Karimakarin
Can you please clarify whether you think throwing is not allowed in consteval or if you have an external requirement that you not use throw?Karimakarin
@FrançoisAndrieux: You can write throw in a constexpr function. You cannot execute any throw statements in constant evaluation; that causes a compile error. consteval only forces any calls to the function to be constant expressions; it doesn't change how they operate. So the standard rules of constexpr apply.Narrate
@NicolBolas, Then why can't throwing be used in consteval to produce a hard error when that code path is executed?Isomerous
@chris: It can. One of the answers mentions this.Narrate
@NicolBolas, Then what was wrong with my original comment?Isomerous
@chris: I was only speaking of assert.Narrate
@NicolBolas But assert seems to work as well. If the element exists, it compiles fine.Cammie
@NicolBolas, (Sorry, I'm multitasking right now and slow to reply. I also probably confused your initial reply with the more general one following it.) According to cppreference, assert should work as intended, even in an executed code path, as of C++17. I actually thought it was since C++14, but I guess either works in a C++20 context. Standardwise, that blurb is in [assertions.assert].Isomerous
"If I add an exception it is thrown at runtime, though." Your function is consteval, it is not thrown at runtime.Faunia
Also please don't edit answers into questions.Faunia
@Faunia - "Your function is consteval, it is not thrown at runtime." if you want, I can record a screen capture and show you that you're wrong. I assume that it means that the Apple Clang compiler isn't entirely compliant with the standard.Viscounty
@Barry, I don't understand why you edited my additional information out of the answer - it contained info which might be relevant to future viewers, who will want to understand or confirm this erroneous behavior.Viscounty
B
7

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

Braxton answered 29/4, 2021 at 15:36 Comment(3)
I would put a comment there to let people know that it is intensional, or just throw a value.Narrate
@NicolBolas: I would have used throw, until I saw that gcc reject it Demo.Braxton
Then GCC is not following C++20 correctly.Narrate
F
17

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.

Faunia answered 29/4, 2021 at 15:39 Comment(2)
gcc rejects the throw though :( DemoBraxton
@Braxton That's a bug they fixed in 10.3Faunia
B
7

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

Braxton answered 29/4, 2021 at 15:36 Comment(3)
I would put a comment there to let people know that it is intensional, or just throw a value.Narrate
@NicolBolas: I would have used throw, until I saw that gcc reject it Demo.Braxton
Then GCC is not following C++20 correctly.Narrate
N
3

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.

Narrate answered 29/4, 2021 at 15:29 Comment(1)
Its (almost) always best to answer the question as code attached in questions is usually just an example of the problem and a way to communicate the solution.Helvetic

© 2022 - 2024 — McMap. All rights reserved.