2

After fiddling with the Compiler Explorer (as well as reading cppref.com on std::optional) for half an hour, I give up. Not much else to say other than I don't understand why this code doesn't compile. Someone please explain this, and maybe show me a workaround if there is one? All the member functions of std::optional I'm using here are constexpr, and indeed should be computable at compile time, given that the optional type here - size_t - is a primitive scalar type.

#include <type_traits>
#include <optional>

template <typename T>
[[nodiscard]] constexpr std::optional<size_t> index_for_type() noexcept
{
    std::optional<size_t> index;
    if constexpr (std::is_same_v<T, int>)
        index = 1;
    else if constexpr (std::is_same_v<T, void>)
        index = 0;

    return index;
}
 
static_assert(index_for_type<int>().has_value());

https://godbolt.org/z/YKh5qT4aP

Violet Giraffe
  • 32,368
  • 48
  • 194
  • 335
  • `index = 1;` is an assignment, and IIRC you can't have that in a constant expression. If you just directly return it works fine: https://godbolt.org/z/Mseh4hjj6 – NathanOliver Jul 02 '21 at 20:20
  • @NathanOliver, I agree the assignment is useless in this reduced example, but it would be nice for the actual code I needed this for. But more importantly, the assignment is also constexpr (conditionally), why doesn't it work at compile time? – Violet Giraffe Jul 02 '21 at 20:22
  • 1
    Isn't this a typo: https://godbolt.org/z/bGrWoo35K ? – Marek R Jul 02 '21 at 20:23
  • @MarekR, did you only change the type of the optional? It wasn't a typo, I intend to return `std::optional` with a fixed type, but it's quite curious that your version works! – Violet Giraffe Jul 02 '21 at 20:25

1 Answers1

6

Let's simply this a bit:

constexpr std::optional<size_t> index_for_type() noexcept
{
    std::optional<size_t> index;
    index = 1;
    return index;
}

static_assert(index_for_type().has_value());

With index = 1; the candidate you're trying to invoke amongst the assignment operators is #4:

template< class U = T >
optional& operator=( U&& value );

Note that this candidate was not originally made constexpr in C++20, it was a recent DR (P2231R1). libstdc++ does not yet implement this change, which is why your example does not compile. As of now, it is perfectly correct C++20 code. The library just hasn't quite caught up yet.


The reason that Marek's suggestion works:

constexpr std::optional<size_t> index_for_type() noexcept
{
    std::optional<size_t> index;
    index = size_t{1};
    return index;
}

static_assert(index_for_type().has_value());

Is because instead of calling assignment operator #4 (which would otherwise continue to not work for the same reason, it's just not constexpr in this implementation yet), it switches to calling operator #3 (which is constexpr):

constexpr optional& operator=( optional&& other ) noexcept(/* see below */);

Why this one? Because #4 has this constraint:

and at least one of the following is true:

  • T is not a scalar type;
  • std::decay_t<U> is not T.

Here, T is size_t (it's the template parameter of the optional specialization) and U is the argument type. In the original case, index = 1, U is int which makes the second bullet hold (int is, indeed, not size_t) and thus this assignment operator is valid. But when we change it to index = size_t{1}, now U becomes size_t, so the second bullet is also false, and we lose this assignment operator as a candidate.

This leaves the copy assignment and move assignment as candidates, of which the latter is better. The move assignment is constexpr in this implementation, so it works.


Of course, the better implementation still would be to avoid assignment and just:

constexpr std::optional<size_t> index_for_type() noexcept
{
    return 1;
}

static_assert(index_for_type().has_value());

Or, in the original function:

template <typename T>
[[nodiscard]] constexpr std::optional<size_t> index_for_type() noexcept
{
    if constexpr (std::is_same_v<T, int>) {
        return 1;
    } else if constexpr (std::is_same_v<T, void>) {
        return 0;
    } else {
        return std::nullopt;
    }
}

This works just fine, even in C++17.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • Thank you very much for the explanation. One overload of `operator=` I didn't notice was not `constexpr`, and I ended up calling that one, of course. – Violet Giraffe Jul 02 '21 at 22:47
  • And I agree that returning the proper value without unnecessary assignment is much cleaner in this case, except this is a simplified example of a larger piece of code where it's not at all that simple to remove the need for assignment. – Violet Giraffe Jul 02 '21 at 22:49