Violet Giraffe
Violet Giraffe

Reputation: 33579

Why is std::optional's assignment operator not usable in compile-time context in this simple example?

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

Upvotes: 2

Views: 795

Answers (1)

Barry
Barry

Reputation: 302852

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.

Upvotes: 6

Related Questions