4

Here's the code I would like to be able to write:

int id1 = not_const_expr_1();
int id10 = not_const_expr_10();

constexpr Device& cD1 = get_device(1);    // 1. ok
constexpr Device& cD10 = get_device(10);  // 2. compile error

Device& cD1 = get_device(1);    // 3. ok
Device& cD10 = get_device(10);  // 4. compile error

Device& D1 = get_device(id1);    // 5. ok
Device& D10 = get_device(id10);  // 6. exception, log message, infinite loop, or something

Here's what I tried:

template<typename T>
T failed(const char*) { while(1); }  // could also throw exception

constexpr Device& dev1 = ...;
constexpr Device& dev42 = ...;

constexpr Device& get_device(uint8_t which) {
    return which == 1 ? dev1 :
           which == 42 ? dev42 :
           // more here, but not important
           failed<Device&>("Device does not exist");
           // string argument just to aid with compiler error message
}

This passes all the above testcases except number 4. Unfortunately, the error is not caught at compile-time, as the compiler finds that for the given argument, get_device is not const_expr.

Is there any way I can work a static_assert in here, without breaking things in the non-constexpr context?

Eric
  • 95,302
  • 53
  • 242
  • 374
  • Perhaps making get_device take a const? – Paul Stelian Apr 12 '17 at 17:06
  • Plus, static_assert can't work with the not-constexpr things. I'm surprised that does not give an error by itself. – Paul Stelian Apr 12 '17 at 17:07
  • You might want to have two versions of get_device, one that will `failed`, and is not constexpr and the other that will `static_assert` and is. But I'm not sure if constexpr can be overloaded in this manner. – OmnipotentEntity Apr 12 '17 at 17:15
  • @OmnipotentEntity: If overloading the same name is not possible, any recommendation for how to write the two functions without duplicating the lookup table? – Eric Apr 12 '17 at 17:16
  • Turn the lookup into a constexpr bare array and simply index (with a bounds check.) Assuming it's contiguous, which might not be the case. – OmnipotentEntity Apr 12 '17 at 17:17
  • @OmnipotentEntity: Not contiguous, and in some cases, not integral ids either – Eric Apr 12 '17 at 17:20
  • In that case I would recommend using 3 functions. One constexpr base implementation that returns an invalid value on failure, and is not visible outside of the translation unit. One constexpr with a static assert. One regular with the runtime failure. – OmnipotentEntity Apr 12 '17 at 17:21
  • 1
    [Trick with `throw`](http://stackoverflow.com/questions/34280729/throw-in-constexpr-function) won't help here? – Zereges Apr 12 '17 at 17:29
  • @Zereges: See [this question](http://stackoverflow.com/q/20461121/102441). That trick is equivalent to my `failed` function – Eric Apr 12 '17 at 17:40
  • You are definitely restricted to 11? – Nir Friedman Apr 12 '17 at 17:44
  • @NirFriedman: Turns out I can actually compile with `-std=gnu++1y` at best. I'd still appreciate answers for newer versions – Eric Apr 12 '17 at 17:51
  • @Eric I'm not sure if that actually helps, sorry I was thinking about some changes that had been made. There's a discussion by Eric Niebler here: http://ericniebler.com/2014/09/27/assert-and-constexpr-in-cxx11/. I'm actually having trouble understanding why you don't get the behavior you want with the code you have; it seems like the right hand side should still be considered a constant expression even if it is being assigned to a non constexpr value. Very odd. – Nir Friedman Apr 12 '17 at 18:30
  • @NirFriedman: The right hand side is not considered a constant expression, because by design `failed` is not either. I'm exploiting this behaviour in cases 1. and 2., but it comes back to bite me in case 4. That blog shows the same effect as the one I'm seeing – Eric Apr 12 '17 at 18:34
  • Very related: http://stackoverflow.com/q/40409323/102441 – Eric Apr 12 '17 at 20:07

1 Answers1

2

Here's a hack that comes close:

#define _return_constexpr(x) static_assert(((void)x, true), ""); return x
template <uint8_t which>
constexpr Device& get_device() {
    _return_constexpr(get_device(which));
}

Which passes the tests as:

constexpr Device& cD1 = get_device(1);      // 1a. ok
constexpr Device& cD1 = get_device<1>();    // 1b. ok
constexpr Device& cD10 = get_device(10);    // 2a. compile error
constexpr Device& cD10 = get_device<10>();  // 2b. compile error

Device& cD1 = get_device(1);      // 3a. ok
Device& cD1 = get_device<1>();    // 3b. ok
Device& cD10 = get_device(10);    // 4a. runtime error (FAIL)
Device& cD10 = get_device<10>();  // 4b. compile error (PASS!)

Device& D1 = get_device(id1);    // 5. ok
Device& D10 = get_device(id10);  // 6. ok

It seems that static_assert is the unique context which forces constexpr evaluation that:

  • Works for any type (a template argument would not accept Device&)
  • Works in a constexpr context (array sizes require a declaration)

Is there a tidier way to write this macro?


Without the requirement that the result itself work in a constexpr context, we can ditch the macro:

template <uint8_t which>
Device& get_device() {
    constexpr auto& ret = get_device(which);
    return ret;
}
Eric
  • 95,302
  • 53
  • 242
  • 374
  • 1
    You need a `void` in there, like `static_assert(((void)x, true), ""); return x`, because if `x` overrides `constexpr operator,` the result could be amusing. `constexpr bool Device::operator,(bool b)const{return false;}` – Yakk - Adam Nevraumont May 29 '17 at 19:25
  • @Yakk: Ugh, I forgot that was a thing. Thanks! – Eric May 29 '17 at 20:00