5

Given a constexpr function, is there a way to create a compile-time error if the function is called at compile-time and return a sentinel value if the function is called at runtime?

I unfortunately can't use exceptions since they are disabled in the build.

This would be used mostly for converting to and from enums and to and from strings. If the developers enter incorrect values, it would be nice to fail the build rather than hope they see the error at runtime, but since we can get values from unknown sources, there is a chance that the value wouldn't be valid and we don't want to crash at runtime.

Demo use-case:

#include <fmt/core.h>

#include <iostream>

// from: https://stackoverflow.com/a/63529662/4461980
// if C++20, we will need a <type_traits> include for std::is_constant_evaluated
#if __cplusplus >= 202002L
#include <type_traits>
#endif
constexpr bool is_constant_evaluated() {
#if __cplusplus >= 202002L
    return std::is_constant_evaluated();
#elif defined(__GNUC__)  // defined for both GCC and clang
    return __builtin_is_constant_evaluated();
#else
    // If the builtin is not available, return a pessimistic result.
    // This way callers will implement everything in a constexpr way.
    return true;
#endif
}

enum class MyEnum { A, B, C, END_OF_ENUM };

constexpr const char* ToString(MyEnum value) {
    switch (value) {
        case MyEnum::A: {
            return "A";
        }
        case MyEnum::B: {
            return "B";
        }
        case MyEnum::C: {
            return "C";
        }
        case MyEnum::END_OF_ENUM:
        default: {
            if (is_constant_evaluated()) {
                // compile time error?
                return "test";
            } else {
                return "UNKNOWN";
            }
        }
    }
    // unreachable?
    return "";
}

int main(int argc, char** argv) {
    // strcitly-speaking not UB since 5 is 0b101 in binary which does not use more bits than the 
    // highest item which would have value 4 which would be 0b100
    // for demo purposes only, don't scream at me
    constexpr auto stringified = ToString(static_cast<MyEnum>(5));
    fmt::print("{}\n", stringified); // prints "test"
    fmt::print("{}\n", ToString(static_cast<MyEnum>(5))); // prints "UNKNOWN"
}

godbolt: https://godbolt.org/z/nYora1b6M

Florian Humblot
  • 1,121
  • 11
  • 29
  • 2
    `if consteval` would be what you need, but that's added with C++23, see https://en.cppreference.com/w/cpp/language/if – fabian May 15 '23 at 09:52
  • You can't use exceptions because they aren't thrown at compile-time. – user207421 May 15 '23 at 10:01
  • 1
    @user207421 It is possible to throw from constexpr (but not catch), this will result in a built failure. – Pepijn Kramer May 15 '23 at 10:11
  • I think this will not work since you are trying to mix a runtime detectable error with compile time reporting. The use of incorrect values should be covered since you use a MyEnum as argument type. IMO The main thing that can go wrong is developers NOT adding conversions for ALL the values that is not detectable at compile time but easily covered by unit tests. – Pepijn Kramer May 15 '23 at 10:23
  • @PepijnKramer in my example I show a case where something goes wrong with a conversion that results in a value for the enum type which is invalid. But apart from that, this technique could also apply to arguments with integral values for example. – Florian Humblot May 15 '23 at 10:47

1 Answers1

2

One option I would consider is to write it like this

constexpr const char* ToString(MyEnum value) {
    switch (value) {
        case MyEnum::A: {
            return "A";
        }
        case MyEnum::B: {
            return "B";
        }
        case MyEnum::C: {
            return "C";
        }
        case MyEnum::END_OF_ENUM:
        default: ;
    }
    if(!is_constant_evaluated()) {
        assert(0 && "Converting value out of range"!);
        return "";
    }
}

A governing principle of constant evaluation is that no undefined behavior in the core language is allowed when the expression is constant-evaluated. Flowing off of the end of a function with a non-void return type is one such instance of UB that a compiler will catch and report.
Users are then led to the end of the function, where hopefully the assert message gives them a clue. In regular runtime evaluation we get the usual assert logic - in the absence of exceptions, as you specified.

StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458