How can I check that assignment of const_reverse_iterator to reverse_iterator is invalid?

  • A+

Consider the following:

using vector_type = std::vector<int>; using const_iterator = typename vector_type::const_iterator; using const_reverse_iterator = typename vector_type::const_reverse_iterator; using iterator = typename vector_type::iterator; using reverse_iterator = typename vector_type::reverse_iterator;  int main() {     static_assert(!std::is_assignable_v<iterator, const_iterator>); // passes     static_assert(!std::is_assignable_v<reverse_iterator, const_reverse_iterator>); // fails     static_assert(std::is_assignable_v<reverse_iterator, const_reverse_iterator>); // passes } 

I can check that assignment of iterator{} = const_iterator{} is not valid, but not an assignment of reverse_iterator{} = const_reverse_iterator{} with this type trait.

This behavior is consistent across gcc 9.0.0, clang 8.0.0, and MSVC 19.00.23506

This is unfortunate, because the reality is that reverse_iterator{} = const_reverse_iterator{} doesn't actually compile with any of the above-mentioned compilers.

How can I reliably check such an assignment is invalid?

This behavior of the type trait implies that the expression

std::declval<reverse_iterator>() = std::declval<const_reverse_iterator>()  

is well formed according to [meta.unary.prop], and this appears consistent with my own attempts at an is_assignable type trait.


This trait passes because the method exists that can be found via overload resolution and it is not deleted.

Actually calling it fails, because the implementation of the method contains code that isn't legal with those two types.

In C++, you cannot test if instantiating a method will result in a compile error, you can only test for the equivalent of overload resolution finding a solution.

The C++ language and standard library originally relied heavily on "well, the method body is only compiled in a template if called, so if the body is invalid the programmer will be told". More modern C++ (both inside and outside the standard library) uses SFINAE and other techniques to make a method "not participate in overload resolution" when its body would not compile.

The constructor of reverse iterator from other reverse iterators is the old style, and hasn't been updated to "not participate in overload resolution" quality.

From n4713, [reverse.iter.cons]/3:

  template<class U> constexpr reverse_iterator(const reverse_iterator<U>& u); 

Effects: Initializes current with u.current.

Notice no mention of "does not participate in overload resolution" or similar words.

The only way to get a trait you want like this would be to

  1. Change (well, fix) the C++ standard

  2. Special case it

I'll leave 1. as an exercise. For 2., you know that reverse iterators are templates over forward iterators.

template<class...Ts> struct my_trait:std::is_assignable<Ts...> {};  template<class T0, class T1> struct my_trait<std::reverse_iterator<T0>, std::reverse_iterator<T1>>:   my_trait<T0, T1> {}; 

and now my_trait is is_assignable except on reverse iterators, where it instead tests assignability of the contained iterators.

(As extra fun, a reverse reverse iterator will work with this trait).

I once had to do something very similar with std::vector<T>::operator<, which also blindly called T<T and didn't SFINAE disable it if that wasn't legal.

It may also be the case that a C++ standard library implementation can make a constructor which would not compile not participate in overload resolution. This change could break otherwise well formed programs, but only by things as ridiculous as your static assert (which would flip) or things logically equivalent.


:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: