Type traits and unevaluated context

278 views Asked by At

cppreference.com says about is_assignable<T,U> type-trait that:

If the expression std::declval<T>() = std::declval<U>() is well-formed in unevaluated context, provides the member constant value equal true. For any other type, value is false. The types T and U must be complete object types, cv void or arrays of unknown bound. Access checks are performed as if from a context unrelated to either type.

But code:

template< std::size_t ...indices, std::size_t ...counter >
auto
invert_indices(std::index_sequence< counter... >)
{
    constexpr std::size_t size = sizeof...(counter);
    constexpr std::size_t inverted[size]{indices...};
    return std::index_sequence< inverted[size - 1 - counter]... >{};
}

template< std::size_t ...indices >
auto
invert_indices()
{
    return invert_indices< indices... >(std::make_index_sequence< sizeof...(indices) >{});
}

template< std::size_t ...indices >
using make_inverted_indices_t = decltype(invert_indices< indices... >());

using L = make_inverted_indices_t< 10, 20, 30 >;
using R = std::index_sequence< 30, 20, 10 >;
static_assert(std::is_same< L, R >{});
static_assert(std::is_assignable< make_inverted_indices_t< 10, 20, 30 > &, R >{});
static_assert(!std::is_assignable< make_inverted_indices_t< 10, 20, 30 > &, int >{});

work, in spite of functions invert_indices are not constexpr and have result type auto.

For context to be unevaluated is it still permittable to look into function bodies during decltype working?

2

There are 2 answers

0
Jarod42 On

invert_indices< indices... >() is not evaluated, but its return type is known. It requires to look at body to know its type because of the auto return type.

so make_inverted_indices_t< 10, 20, 30 > is std::index_sequence< 30, 20, 10 > which is a complete object type.

0
Yakk - Adam Nevraumont On

The types produced are not part of is_assignable. T and U are produced completely independently of what the semantics of is_assignable are.

The docs around is_assignable describe what it does to the types. But invert_indexes is evaluated by make_inverted_indexes_t, not by is_assignable. And make_inverted_indexes_t clearly evaluates the body of invert_indexes.

All of this happens before is_assignable is passed the types.

If we instead look at things that happen after is_assignable is passed the types, here I create a toy type:

template<size_t N>
struct foo {
  template<std::size_t K>
  decltype(auto) operator=(foo<K> const& in){
    static_assert( (K<N) );
    return in;
  }
};

which in the immediate context looks like it can be assigned to-from every other foo, and

template<size_t N>
struct foo {
  template<std::size_t K, class=std::enable_if_t<(K<N)>>
  decltype(auto) operator=(foo<K> const& in){
    static_assert( (K<N) );
    return in;
  }
};

which adds a SFINAE guard. If we run these tests:

using L = foo<10>;
using R = foo<11>;
static_assert(!std::is_same< L, R >{});
static_assert(!std::is_assignable< L &, R >{});
static_assert(std::is_assignable< R &, L >{});
static_assert(!std::is_assignable< L &, std::string >{});

in the first case, we get hard errors as the body of operator= is evaluated. In the second case, the SFINAE guard kicks in, and everything passes.

Live example.

So what goes on is that even in the unevaluated context, the bodies of functions are evaluated if their return type is auto, at least in practice.