4

I am looking for a way to raise a compile-time error from a constexpr function. Since I am on an embedded system, C++ exceptions need to remain disabled (GCC flag -fno-exceptions). Thus, the default way of error reporting seems to be infeasible.

A possible way described in constexpr error at compile-time, but no overhead at run-time is to call a non-constexpr function, which throws an error if compile-time implementation is forced. However, this solution gives rather unreadable error messages and the implementation is forced to return garbage return values in order to silence "control may reach end of non-void function" warnings.

Is there a better way, which allows to provide a custom error message?

Please note, that I am aware of static_assert and the possibility to convert the function to a template. However, static_assert needs to reassemble the quite complex logic of the switch-blocks of my use-case in order to throw an error, which is error-prone and clumsy.

Example use-case:

constexpr SpiDmaTxStreams spiDmaTxStream(DmaId dmaId, DmaStreamId streamId) {
    switch (dmaId) {
        case DmaId::DMA_1:
            switch (streamId) {
                case DmaStreamId::Stream_4:
                    return SpiDmaTxStreams::Dma1Stream4;
                // ...
                default:
                    break;
            }
            break;
        case DmaId::DMA_2:
            switch (streamId) {
                case DmaStreamId::Stream_1:
                    return SpiDmaTxStreams::Dma2Stream1;
                // ...
                default:
                    break;
            }
            break;
    }
    // report compile-time error "invalid DMA-stream combination"
}
Alexander
  • 1,068
  • 6
  • 22
  • @Holt This does not work, because `static_assert` is evaluated by the compiler independently of the control flow. Thus, it would always or never fire or the assertion logic needs to be repeated. – Alexander May 03 '18 at 16:51
  • *"the implementation is forced to return garbage return values in order to silence "control may reach end of non-void function" warnings."* `[[noreturn]]` attribute might be used. – Jarod42 May 03 '18 at 16:58
  • Yes, you're right... Do you plan on only using this with compile-time `dmaId` and `streamId`? What are `DmaId` and `DmaStreamId`? Maybe you could use some template here with specialization. – Holt May 03 '18 at 16:59
  • @Jarod42 according to http://en.cppreference.com/w/cpp/language/attributes `[[noreturn]]` renders all return values to undefined behavior. This is not what I want, because in valid cases there are return values. I only want to omit return in "illegal" situations which produce a compile-time error. – Alexander May 03 '18 at 17:04
  • @Holt Yes, `dmaId` and `streamId` are literals or constexpr (enum class members) and the whole function is only expected to work at compile-time. – Alexander May 03 '18 at 17:06
  • So isn't clear to me why you exclude a template solution; it seems to me the simpler way. – max66 May 03 '18 at 17:08
  • I meant to mark the non `constexpr` function as `[[noreturn]]`. – Jarod42 May 03 '18 at 17:08
  • *"this solution gives rather unreadable error messages"*: what do you get ? I got something like `main.cpp:12:21: in constexpr expansion of 'f(0)' main.cpp:8:16: error: call to non-constexpr function 'void failed(const char*)' else { failed("Invalid arg"); }` which seems clear enough. – Jarod42 May 03 '18 at 17:11
  • @max66 Because for my use case a moderate switch block seems to be more readable than a giant boolean expression within a single static_assert. – Alexander May 03 '18 at 17:16
  • Well... a solution template struct based seems to me a lot more readable; I propose you it as an answer... hoping that can helps. – max66 May 03 '18 at 17:19

3 Answers3

3

One way to trigger a constexpr compile error is to trigger UB. The simplest way to trigger UB is via __builtin_unreachable(). That unfortunately doesn't allow for a message, but we could wrap it in a macro.

As an example this program:

#define CONSTEXPR_FAIL(...) __builtin_unreachable()

constexpr int foo(int a, int b) {
    switch (a) {
    case 0:
        return b;
    case 1:
        if (b == 2) return 3;
        break;
    }

    CONSTEXPR_FAIL("Mismatch between a and b");
}

int main() {
    static_assert(foo(0, 2) == 2, "!");

    // constexpr int i = foo(2, 2);
}

Compiles fine on gcc 7.2 and clang 5.0 with c++14. If you un-comment the call to foo(2,2), gcc emits:

<source>: In function 'int main()':
<source>:18:26:   in constexpr expansion of 'foo(2, 2)'
<source>:1:50: error: '__builtin_unreachable()' is not a constant expression
 #define CONSTEXPR_FAIL(...) __builtin_unreachable()
                             ~~~~~~~~~~~~~~~~~~~~~^~
<source>:12:5: note: in expansion of macro 'CONSTEXPR_FAIL'
     CONSTEXPR_FAIL("Mismatch between a and b");
     ^~~~~~~~~~~~~~

and clang emits:

<source>:18:19: error: constexpr variable 'i' must be initialized by a constant expression
    constexpr int i = foo(2, 2);
                  ^   ~~~~~~~~~
<source>:12:5: note: subexpression not valid in a constant expression
    CONSTEXPR_FAIL("Mismatch between a and b");
    ^
<source>:1:29: note: expanded from macro 'CONSTEXPR_FAIL'
#define CONSTEXPR_FAIL(...) __builtin_unreachable()
                            ^
<source>:18:23: note: in call to 'foo(2, 2)'
    constexpr int i = foo(2, 2);
                      ^

Does this work for you? It's not quite a static_assert in that the compiler doesn't emit the message for you directly, but the it does get the compiler to point to the correct line and the message is going to be in the call stack.

hellow
  • 12,430
  • 7
  • 56
  • 79
Barry
  • 286,269
  • 29
  • 621
  • 977
  • Thank you. Although it is less portable, this is the most helpful answer so far. However it only works with my code shown above if CONSTEXPR_FAIL is put instead of the `break`s in the inner switch blocks. If I only put it after the outer switch, then everything compiles without error even with illegal arguments. Any idea why that is? – Alexander May 03 '18 at 20:35
  • @Alexander Can you provide an [mcve] demonstrating that? If the code flow gets to the `__builtin_unreachable()`, it should die. Are you sure you're not hitting some other `return` by accident? – Barry May 03 '18 at 20:45
  • 1
    You are right of course. While preparing a minimal example I noticed the `break` statements were missing in the outer switch cases. So everything works fine. I updated my example code. – Alexander May 03 '18 at 21:34
  • 1
    This triggers undefined behaviour during runtime, if you happen to call it at runtime. Since calling any non-constexpr function is sufficient, I propose either writing your own function or calling something with defined behaviour like `::std::abort()` instead of `__builtin_unreachable` – WorldSEnder May 03 '18 at 21:47
-1

Sorry, because you asked a completely different solution, but if

dmaId and streamId are literals or constexpr (enum class members) and the whole function is only expected to work at compile-time

to pass dmaId and streamId as not-template parameter seems to me the wrong way.

It seems to me a lot simpler something as follows (sorry: code not tested)

// generic foo: to force a comprehensible error message 
template <DmaId I1, DmaStreamId I2>
struct foo
 {
   static_assert( (I1 ==  DmaId::DMA_1) && (I2 == DmaStreamId::Stream_4),
                  "your error message here" );
 };

// specialization with all acceptable combinations

template <>
struct foo<DmaId::DMA_1, DmaStreamId::Stream_4>
 { static constexpr auto value = SpiDmaTxStreams::Dma1Stream4; };

// ...

template <>
struct foo<DmaId::DMA_2, DmaStreamId::Stream_1>
 { static constexpr auto value = SpiDmaTxStreams::Dma2Stream1; };

// ...

So, instead of

constexpr value id = spiDmaTxStream(DmaId::DMA_2, DmaStreamId::Stream_1);

you can write

constexpr value id = foo<DmaId::DMA_2, DmaStreamId::Stream_1>::value;
max66
  • 65,235
  • 10
  • 71
  • 111
  • This is IMO the best solution but you should simply left the non-specialised template undefined. – Holt May 03 '18 at 17:22
  • @Holt - Usually I'm agree; but, in this case, the OP ask "to provide a custom error message". – max66 May 03 '18 at 17:25
  • As I expected, this does not scale, because the boolean expression in the non-specialised template has to repeat all logic of the specialized templates. (There are many DMA/Stream combinations which are valid or invalid, not only a single as shown in your solution.) This is why i explicitly excluded this solution in my question. Thanks anyway! – Alexander May 03 '18 at 17:30
  • @Alexander As I said, simply do not implement the generic case, you won't get a message as pretty as this one but it should nonetheless be easily understandable. – Holt May 03 '18 at 17:31
  • @Holt No, sorry. Blasting out a bunch of hardly understandable compiler messages about a constant expansion is not enough for a well defined library, where barely half of the users know the constexpr keyword. :) – Alexander May 03 '18 at 17:34
  • @Alexander - it's difficult to give an answer without the knowledge of valid combinations; anyway I suggest to to try to use `dmaId` and `streamId` as template values; in a struct solution or in a template function solution – max66 May 03 '18 at 17:39
  • 1
    @Alexander - obviously, if you can use C++17 (so `if constexpr`), all is simpler. – max66 May 03 '18 at 17:41
  • @max66 wow, thanks for pointing out `if constexpr`. I'm not sure yet how it might help, but it is really interesting. btw, I did not down vote. – Alexander May 03 '18 at 20:28
  • @Alexander The compiler messages for such error are not hardly understandable, the error from clang is `error: implicit instantiation of undefined template 'spiDmaTxStream'`, which is pretty clear. If users of your code does not know `constexpr`, you're going to face bigger issue, like what if someone call your function with non-constexpr value? You're going to reach "urneachable" code at runtime. Using template, you cannot use runtime values. – Holt May 04 '18 at 05:41
  • @Alexander And you can also make a stupid check inside `static_assert` for this to work without having to rewrite your logic, e.g., `static_assert(I1 != I1, "Whatever... ");` (in the original code). – Holt May 04 '18 at 05:43
  • @Holt - but a `static_assert()` check not too stupid: see [this question](https://stackoverflow.com/questions/14637356/static-assert-fails-compilation-even-though-template-function-is-called-nowhere) and the answer from Jonathan Wakely; I'm not sure that `static_assert(I1 != I1` is acceptable – max66 May 04 '18 at 09:14
  • @Alexander - Yes: `if constexpr` is really interesting; unfortunately is available only from C++17 (and you tagged C++14). But, to use it, you need `dmaId` and `streamId` ever known at compile time (like for `static_assert()`); so you need `template constexpr SpiDmaTxStreams spiDmaTxStream() { if constexpr (DmaId::DMA_1 == dmaId) { ` – max66 May 04 '18 at 09:22
  • @Alexander - by the way... have you checked if my other solution (a `contsexpr` function that initialize a template value) works? Just curious... – max66 May 04 '18 at 09:24
  • @max66 `static_assert(I1 != I1, "Whatever... ");` works with clang, not sure it's standard though, but even in this case you can likely find a condition that never works (e.g. `static_cast(I1) == std::numeric_limits::max()`), it depends on the actual type. – Holt May 04 '18 at 09:38
-1

If you can add a special error value in SpiDmaTxStreams enum... say SpiDmaTxStreams::ErrorValue... I propose another solution, again based on a template struct but with reverted logic: the not-specialized struct and a single specialized version for static_error messagge.

I mean... if you return SpiDmaTxStreams::ErrorValue in case of unacceptable combination

constexpr SpiDmaTxStreams spiDmaTxStream(DmaId dmaId, DmaStreamId streamId) {
    switch (dmaId) {
        case DmaId::DMA_1:
            switch (streamId) {
                case DmaStreamId::Stream_4:
                    return SpiDmaTxStreams::Dma1Stream4;
                // ...
                default:
                    return SpiDmaTxStreams::ErrorValue; // <<---- add this
                    break;
            }
        case DmaId::DMA_2:
            switch (streamId) {
                case DmaStreamId::Stream_1:
                    return SpiDmaTxStreams::Dma2Stream1;
                // ...
                default:
                    return SpiDmaTxStreams::ErrorValue; // <<---- add this
                    break;
            }
    }
    // report compile-time error "invalid DMA-stream combination"
}

You can call spiDmaTxStream() to give value to a template value (caution: code not tested) as follows

template <DmaId I1, DmaStreamId I2,
          SpiDmaTxStreams IR = spiDmaTxStream(I1, I2)>
struct foo
 { static constexpr auto value = IR; };


template <DmaId I1, DmaStreamId I2>
struct foo<I1, I2, SpiDmaTxStreams::ErrorValue>
 {
   // where DmaId::DMA_1/DmaStreamId::Stream_4 is an 
   // acceptable combination
   static_assert( (I1 == DmaId::DMA_1) && (I2 == DmaStreamId::Stream_4),
                  "your error message here" );
 };

and, again, instead of

constexpr value id = spiDmaTxStream(DmaId::DMA_2, DmaStreamId::Stream_1);

you can write

constexpr value id = foo<DmaId::DMA_2, DmaStreamId::Stream_1>::value;

If the dmaId/streamId is unacceptable, spiDmaTxStream() return SpiDmaTxStreams::ErrorValue, so the foo specialized version is activated and the static_error() message is in charge.

max66
  • 65,235
  • 10
  • 71
  • 111